模糊测试之书-十九-

模糊测试之书(十九)

原文:exploringjs.com/ts/book/index.html

译者:飞龙

协议:CC BY-NC-SA 4.0

计时器

原文:www.fuzzingbook.org/html/Timer.html

本笔记本中的代码有助于测量时间。

先决条件

  • 本笔记本需要一些对 Python 高级概念的理解,特别是

    • 课程

    • Python 的 with 语句

    • 测量时间

概述

要使用本章提供的代码,请编写

>>> from fuzzingbook.Timer import <identifier> 

然后利用以下功能。

Timer类允许您测量经过的实际时间(以分数秒为单位)。它的典型用法是与with子句结合:

>>> with Timer() as t:
>>>     some_long_running_function()
>>> t.elapsed_time()
0.020704666967503726 

测量时间

Timer类允许在代码执行期间测量经过的时间。

import [bookutils.setup](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) 
import [time](https://docs.python.org/3/library/time.html) 
def clock() -> float:
  """
 Return the number of fractional seconds elapsed since some point of reference.
 """
    return time.perf_counter() 
from [types](https://docs.python.org/3/library/types.html) import TracebackType 
class Timer:
    def __init__(self) -> None:
  """Constructor"""
        self.start_time = clock()
        self.end_time = None

    def __enter__(self) -> Any:
  """Begin of `with` block"""
        self.start_time = clock()
        self.end_time = None
        return self

    def __exit__(self, exc_type: Type, exc_value: BaseException,
                 tb: TracebackType) -> None:
  """End of `with` block"""
        self.end_time = clock()

    def elapsed_time(self) -> float:
  """Return elapsed time in seconds"""
        if self.end_time is None:
            # still running
            return clock() - self.start_time
        else:
            return self.end_time - self.start_time 

这里有一个例子:

def some_long_running_function() -> None:
    i = 1000000
    while i > 0:
        i -= 1 
print("Stopping total time:")
with Timer() as t:
    some_long_running_function()
print(t.elapsed_time()) 
Stopping total time:
0.020959541958291084

print("Stopping time in between:")
with Timer() as t:
    for i in range(10):
        some_long_running_function()
        print(t.elapsed_time()) 
Stopping time in between:
0.01922783302143216
0.039810792019125074
0.06000966700958088
0.08149537502322346
0.10375195799861103
0.12619870802154765
0.14785908302292228
0.16952008299995214
0.19183695799438283
0.21232758299447596

好了,各位——享受吧!

经验教训

  • 使用Timer类,测量经过的时间非常容易。

Creative Commons License 本项目的内容受Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议许可。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码,受MIT 许可协议许可。 最后更改:2023-11-11 18:25:47+01:00 • 引用 • 版权信息

如何引用本作品

Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, 和 Christian Holler: "Timer". In Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler, "The Fuzzing Book", www.fuzzingbook.org/html/Timer.html. Retrieved 2023-11-11 18:25:47+01:00.

@incollection{fuzzingbook2023:Timer,
    author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
    booktitle = {The Fuzzing Book},
    title = {Timer},
    year = {2023},
    publisher = {CISPA Helmholtz Center for Information Security},
    howpublished = {\url{https://www.fuzzingbook.org/html/Timer.html}},
    note = {Retrieved 2023-11-11 18:25:47+01:00},
    url = {https://www.fuzzingbook.org/html/Timer.html},
    urldate = {2023-11-11 18:25:47+01:00}
}

超时

原文:www.fuzzingbook.org/html/Timeout.html

本笔记本中的代码有助于在给定时间后中断执行。

先决条件

  • 这个笔记本需要对 Python 的高级概念有所了解,特别是

    • Python 的 with 语句

    • Python 的 signal 函数

    • 测量时间

概述

要 使用本章节提供的代码,请编写

>>> from fuzzingbook.Timeout import <identifier> 

然后利用以下功能。

Timeout 类在给定超时时间到期后抛出 TimeoutError 异常。它的典型用法是与 with 语句结合使用:

>>> try:
>>>     with Timeout(0.2):
>>>         some_long_running_function()
>>>     print("complete!")
>>> except TimeoutError:
>>>     print("Timeout!")
Timeout! 

注意:在 Unix/Linux 系统上,Timeout 类使用 SIGALRM 信号(中断)来实现超时;这对跟踪代码的性能没有影响。在其他系统(特别是 Windows)上,Timeout 使用 sys.settrace() 函数在每行代码后检查计时器,这会影响跟踪代码的性能。

测量时间

Timeout 类允许在给定时间间隔后中断某些代码执行。

import [bookutils.setup](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) 
import [time](https://docs.python.org/3/library/time.html) 
from [types](https://docs.python.org/3/library/types.html) import FrameType, TracebackType 

变体 1:Unix(使用信号,高效)

import [signal](https://docs.python.org/3/library/signal.html) 
class SignalTimeout:
  """Execute a code block raising a timeout."""

    def __init__(self, timeout: Union[int, float]) -> None:
  """
 Constructor. Interrupt execution after `timeout` seconds.
 """
        self.timeout = timeout
        self.old_handler: Any = signal.SIG_DFL
        self.old_timeout = 0.0

    def __enter__(self) -> Any:
  """Begin of `with` block"""
        # Register timeout() as handler for signal 'SIGALRM'"
        self.old_handler = signal.signal(signal.SIGALRM, self.timeout_handler)
        self.old_timeout, _ = signal.setitimer(signal.ITIMER_REAL, self.timeout)
        return self

    def __exit__(self, exc_type: Type, exc_value: BaseException,
                 tb: TracebackType) -> None:
  """End of `with` block"""
        self.cancel()
        return  # re-raise exception, if any

    def cancel(self) -> None:
  """Cancel timeout"""
        signal.signal(signal.SIGALRM, self.old_handler)
        signal.setitimer(signal.ITIMER_REAL, self.old_timeout)

    def timeout_handler(self, signum: int, frame: Optional[FrameType]) -> None:
  """Handle timeout (SIGALRM) signal"""
        raise TimeoutError() 

这里是一个例子:

def some_long_running_function() -> None:
    i = 10000000
    while i > 0:
        i -= 1 
try:
    with SignalTimeout(0.2):
        some_long_running_function()
        print("Complete!")
except TimeoutError:
    print("Timeout!") 
Timeout!

变体 2:通用/Windows(使用跟踪,效率不高)

import [sys](https://docs.python.org/3/library/sys.html) 
class GenericTimeout:
  """Execute a code block raising a timeout."""

    def __init__(self, timeout: Union[int, float]) -> None:
  """
 Constructor. Interrupt execution after `timeout` seconds.
 """

        self.seconds_before_timeout = timeout
        self.original_trace_function: Optional[Callable] = None
        self.end_time: Optional[float] = None

    def check_time(self, frame: FrameType, event: str, arg: Any) -> Callable:
  """Tracing function"""
        if self.original_trace_function is not None:
            self.original_trace_function(frame, event, arg)

        current_time = time.time()
        if self.end_time and current_time >= self.end_time:
            raise TimeoutError

        return self.check_time

    def __enter__(self) -> Any:
  """Begin of `with` block"""
        start_time = time.time()
        self.end_time = start_time + self.seconds_before_timeout

        self.original_trace_function = sys.gettrace()
        sys.settrace(self.check_time)
        return self

    def __exit__(self, exc_type: type, 
                 exc_value: BaseException, tb: TracebackType) -> Optional[bool]:
  """End of `with` block"""
        self.cancel()
        return None  # re-raise exception, if any

    def cancel(self) -> None:
  """Cancel timeout"""
        sys.settrace(self.original_trace_function) 

再次,我们的例子:

try:
    with GenericTimeout(0.2):
        some_long_running_function()
        print("Complete!")
except TimeoutError:
    print("Timeout!") 
Timeout!

选择正确的变体

Timeout: Type[SignalTimeout] = SignalTimeout if hasattr(signal, 'SIGALRM') else GenericTimeout 

练习

创建一个在 Windows 上高效运行的 Timeout 变体。请注意,如何在编程论坛上这是一个长期争论的问题。

Creative Commons License 本项目的内容受 Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议 的许可。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码,受 MIT 许可协议 的许可。 最后更改:2023-11-11 18:25:46+01:00 • 引用 • 版权信息

如何引用本作品

Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler: "Timeout". In Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler, "The Fuzzing Book", www.fuzzingbook.org/html/Timeout.html. Retrieved 2023-11-11 18:25:46+01:00.

@incollection{fuzzingbook2023:Timeout,
    author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
    booktitle = {The Fuzzing Book},
    title = {Timeout},
    year = {2023},
    publisher = {CISPA Helmholtz Center for Information Security},
    howpublished = {\url{https://www.fuzzingbook.org/html/Timeout.html}},
    note = {Retrieved 2023-11-11 18:25:46+01:00},
    url = {https://www.fuzzingbook.org/html/Timeout.html},
    urldate = {2023-11-11 18:25:46+01:00}
}

类图

原文:www.fuzzingbook.org/html/ClassDiagram.html

这是一个简单的类图查看器。针对书籍进行了定制。

先决条件

  • 在此处参考早期章节作为笔记本,如下所示: 早期章节。
import [bookutils.setup](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) 

概述

要使用本章提供的代码(Importing.html),请编写

>>> from fuzzingbook.ClassDiagram import <identifier> 

然后利用以下功能。

函数 display_class_hierarchy() 显示给定类(或类列表)的类层次结构。

  • 关键字参数 public_methods,如果提供,是一个要由客户端使用的“公共”方法列表(默认:所有带有文档字符串的方法)。

  • 关键字参数 abstract_classes,如果提供,是一个要显示为“抽象”的类列表(即带有草书类名)。

>>> display_class_hierarchy(D_Class, abstract_classes=[A_Class]) 

D_Class <a xlink:href="#" xlink:title="class D_Class:

从多个超类继承的子类。

附带相当长但无意义的文档。">D_Class <a xlink:href="#" xlink:title="foo(self) -> None:

一架二战时期的 foo 战斗机。">foo() B_Class <a xlink:href="#" xlink:title="class B_Class:

继承了一些方法的子类。">B_Class VAR <a xlink:href="#" xlink:title="bar(self, qux: Any = None, bartender: int = 42) -> None:

一个 qux 走进了一家酒吧。

bartender 是一个可选属性。">bar() <a xlink:href="#" xlink:title="foo(self) -> None:

一架二战时期的 foo 战斗机。">foo() D_Class->B_Class C_Class <a xlink:href="#" xlink:title="class C_Class:

一个注入某些方法的类">C_Class qux() D_Class->C_Class A_Class <a xlink:href="#" xlink:title="class A_Class:

一个正确完成 A 任务的类。

附带较长的文档字符串。">A_Class <a xlink:href="#" xlink:title="foo(self) -> None:

《辉煌的 Foo 的冒险》">foo() <a xlink:href="#" xlink:title="quux(self) -> None:

一个未被使用的。">quux() second() B_Class->A_Class 图例 图例 •  public_method() •  private_method() •  overloaded_method() 将鼠标悬停在名称上以查看文档

获取类层次结构

import [inspect](https://docs.python.org/3/library/inspect.html) 

使用 mro(),我们可以访问类层次结构。我们确保避免由 class X(X) 创建的重复项。

def class_hierarchy(cls: Type) -> List[Type]:
    superclasses = cls.mro()
    hierarchy = []
    last_superclass_name = ""

    for superclass in superclasses:
        if superclass.__name__ != last_superclass_name:
            hierarchy.append(superclass)
            last_superclass_name = superclass.__name__

    return hierarchy 

这里有一个例子:

class A_Class:
  """A Class which does A thing right.
 Comes with a longer docstring."""

    def foo(self) -> None:
  """The Adventures of the glorious Foo"""
        pass

    def quux(self) -> None:
  """A method that is not used."""
        pass 
class A_Class(A_Class):
    # We define another function in a separate cell.

    def second(self) -> None:
        pass 
class B_Class(A_Class):
  """A subclass inheriting some methods."""

    VAR = "A variable"

    def foo(self) -> None:
  """A WW2 foo fighter."""
        pass

    def bar(self, qux: Any = None, bartender: int = 42) -> None:
  """A qux walks into a bar.
 `bartender` is an optional attribute."""
        pass 
SomeType = List[Optional[Union[str, int]]] 
class C_Class:
  """A class injecting some method"""

    def qux(self, arg: SomeType) -> SomeType:
        return arg 
class D_Class(B_Class, C_Class):
  """A subclass inheriting from multiple superclasses.
 Comes with a fairly long, but meaningless documentation."""

    def foo(self) -> None:
        B_Class.foo(self) 
class D_Class(D_Class):
    pass  # An incremental addiiton that should not impact D's semantics 
class_hierarchy(D_Class) 
[__main__.D_Class,
 __main__.B_Class,
 __main__.A_Class,
 __main__.C_Class,
 object]

获取类树

我们可以使用 __bases__ 来获取直接基类。

D_Class.__bases__ 
(__main__.D_Class,)

class_tree() 返回一个类树,使用具有相同名称的“最低”(最专用)的类。

def class_tree(cls: Type, lowest: Optional[Type] = None) -> List[Tuple[Type, List]]:
    ret = []
    for base in cls.__bases__:
        if base.__name__ == cls.__name__:
            if not lowest:
                lowest = cls
            ret += class_tree(base, lowest)
        else:
            if lowest:
                cls = lowest
            ret.append((cls, class_tree(base)))

    return ret 
class_tree(D_Class) 
[(__main__.D_Class, [(__main__.B_Class, [(__main__.A_Class, [])])]),
 (__main__.D_Class, [(__main__.C_Class, [])])]

class_tree(D_Class)[0][0] 
__main__.D_Class

assert class_tree(D_Class)[0][0] == D_Class 

class_set() 将树扁平化为集合:

def class_set(classes: Union[Type, List[Type]]) -> Set[Type]:
    if not isinstance(classes, list):
        classes = [classes]

    ret = set()

    def traverse_tree(tree: List[Tuple[Type, List]]) -> None:
        for (cls, subtrees) in tree:
            ret.add(cls)
            for subtree in subtrees:
                traverse_tree(subtrees)

    for cls in classes:
        traverse_tree(class_tree(cls))

    return ret 
class_set(D_Class) 
{__main__.A_Class, __main__.B_Class, __main__.C_Class, __main__.D_Class}

assert A_Class in class_set(D_Class) 
assert B_Class in class_set(D_Class) 
assert C_Class in class_set(D_Class) 
assert D_Class in class_set(D_Class) 
class_set([B_Class, C_Class]) 
{__main__.A_Class, __main__.B_Class, __main__.C_Class}

获取文档

A_Class.__doc__ 
A_Class.__bases__[0].__doc__ 
'A Class which does A thing right.\n    Comes with a longer docstring.'

A_Class.__bases__[0].__name__ 
'A_Class'

D_Class.foo 
<function __main__.D_Class.foo(self) -> None>

D_Class.foo.__doc__ 
A_Class.foo.__doc__ 
'The Adventures of the glorious Foo'

def docstring(obj: Any) -> str:
    doc = inspect.getdoc(obj)
    return doc if doc else "" 
docstring(A_Class) 
'A Class which does A thing right.\nComes with a longer docstring.'

docstring(D_Class.foo) 
'A WW2 foo fighter.'

def unknown() -> None:
    pass 
docstring(unknown) 
''

import [html](https://docs.python.org/3/library/html.html) 
import [re](https://docs.python.org/3/library/re.html) 
def escape(text: str) -> str:
    text = html.escape(text)
    assert '<' not in text
    assert '>' not in text
    text = text.replace('{', '&#x7b;')
    text = text.replace('|', '&#x7c;')
    text = text.replace('}', '&#x7d;')
    return text 
escape("f(foo={})") 
'f(foo=&#x7b;&#x7d;)'

def escape_doc(docstring: str) -> str:
    DOC_INDENT = 0
    docstring = "&#x0a;".join(
        ' ' * DOC_INDENT + escape(line).strip()
        for line in docstring.split('\n')
    )
    return docstring 
print(escape_doc("'Hello\n {You|Me}'")) 
&#x27;Hello&#x0a;&#x7b;You&#x7c;Me&#x7d;&#x27;

获取方法和变量

inspect.getmembers(D_Class) 
[('VAR', 'A variable'),
 ('__class__', type),
 ('__delattr__', <slot wrapper '__delattr__' of 'object' objects>),
 ('__dict__', mappingproxy({'__module__': '__main__', '__doc__': None})),
 ('__dir__', <method '__dir__' of 'object' objects>),
 ('__doc__', None),
 ('__eq__', <slot wrapper '__eq__' of 'object' objects>),
 ('__format__', <method '__format__' of 'object' objects>),
 ('__ge__', <slot wrapper '__ge__' of 'object' objects>),
 ('__getattribute__', <slot wrapper '__getattribute__' of 'object' objects>),
 ('__getstate__', <method '__getstate__' of 'object' objects>),
 ('__gt__', <slot wrapper '__gt__' of 'object' objects>),
 ('__hash__', <slot wrapper '__hash__' of 'object' objects>),
 ('__init__', <slot wrapper '__init__' of 'object' objects>),
 ('__init_subclass__', <function D_Class.__init_subclass__>),
 ('__le__', <slot wrapper '__le__' of 'object' objects>),
 ('__lt__', <slot wrapper '__lt__' of 'object' objects>),
 ('__module__', '__main__'),
 ('__ne__', <slot wrapper '__ne__' of 'object' objects>),
 ('__new__', <function object.__new__(*args, **kwargs)>),
 ('__reduce__', <method '__reduce__' of 'object' objects>),
 ('__reduce_ex__', <method '__reduce_ex__' of 'object' objects>),
 ('__repr__', <slot wrapper '__repr__' of 'object' objects>),
 ('__setattr__', <slot wrapper '__setattr__' of 'object' objects>),
 ('__sizeof__', <method '__sizeof__' of 'object' objects>),
 ('__str__', <slot wrapper '__str__' of 'object' objects>),
 ('__subclasshook__', <function D_Class.__subclasshook__>),
 ('__weakref__', <attribute '__weakref__' of 'A_Class' objects>),
 ('bar',
  <function __main__.B_Class.bar(self, qux: Any = None, bartender: int = 42) -> None>),
 ('foo', <function __main__.D_Class.foo(self) -> None>),
 ('quux', <function __main__.A_Class.quux(self) -> None>),
 ('qux',
  <function __main__.C_Class.qux(self, arg: List[Union[int, str, NoneType]]) -> List[Union[int, str, NoneType]]>),
 ('second', <function __main__.A_Class.second(self) -> None>)]

def class_items(cls: Type, pred: Callable) -> List[Tuple[str, Any]]:
    def _class_items(cls: Type) -> List:
        all_items = inspect.getmembers(cls, pred)
        for base in cls.__bases__:
            all_items += _class_items(base)

        return all_items

    unique_items = []
    items_seen = set()
    for (name, item) in _class_items(cls):
        if name not in items_seen:
            unique_items.append((name, item))
            items_seen.add(name)

    return unique_items 
def class_methods(cls: Type) -> List[Tuple[str, Callable]]:
    return class_items(cls, inspect.isfunction) 
def defined_in(name: str, cls: Type) -> bool:
    if not hasattr(cls, name):
        return False

    defining_classes = []

    def search_superclasses(name: str, cls: Type) -> None:
        if not hasattr(cls, name):
            return

        for base in cls.__bases__:
            if hasattr(base, name):
                defining_classes.append(base)
                search_superclasses(name, base)

    search_superclasses(name, cls)

    if any(cls.__name__ != c.__name__ for c in defining_classes):
        return False  # Already defined in superclass

    return True 
assert not defined_in('VAR', A_Class) 
assert defined_in('VAR', B_Class) 
assert not defined_in('VAR', C_Class) 
assert not defined_in('VAR', D_Class) 
def class_vars(cls: Type) -> List[Any]:
    def is_var(item: Any) -> bool:
        return not callable(item)

    return [item for item in class_items(cls, is_var) 
            if not item[0].startswith('__') and defined_in(item[0], cls)] 
class_methods(D_Class) 
[('bar',
  <function __main__.B_Class.bar(self, qux: Any = None, bartender: int = 42) -> None>),
 ('foo', <function __main__.D_Class.foo(self) -> None>),
 ('quux', <function __main__.A_Class.quux(self) -> None>),
 ('qux',
  <function __main__.C_Class.qux(self, arg: List[Union[int, str, NoneType]]) -> List[Union[int, str, NoneType]]>),
 ('second', <function __main__.A_Class.second(self) -> None>)]

class_vars(B_Class) 
[('VAR', 'A variable')]

我们只对

  • 在该类中定义的函数

  • 带有文档字符串的函数

def public_class_methods(cls: Type) -> List[Tuple[str, Callable]]:
    return [(name, method) for (name, method) in class_methods(cls) 
            if method.__qualname__.startswith(cls.__name__)] 
def doc_class_methods(cls: Type) -> List[Tuple[str, Callable]]:
    return [(name, method) for (name, method) in public_class_methods(cls) 
            if docstring(method) is not None] 
public_class_methods(D_Class) 
[('foo', <function __main__.D_Class.foo(self) -> None>)]

doc_class_methods(D_Class) 
[('foo', <function __main__.D_Class.foo(self) -> None>)]

def overloaded_class_methods(classes: Union[Type, List[Type]]) -> Set[str]:
    all_methods: Dict[str, Set[Callable]] = {}
    for cls in class_set(classes):
        for (name, method) in class_methods(cls):
            if method.__qualname__.startswith(cls.__name__):
                all_methods.setdefault(name, set())
                all_methods[name].add(cls)

    return set(name for name in all_methods if len(all_methods[name]) >= 2) 
overloaded_class_methods(D_Class) 
{'foo'}

使用方法名称绘制类层次结构

from [inspect](https://docs.python.org/3/library/inspect.html) import signature 
import [warnings](https://docs.python.org/3/library/warnings.html) 
import [os](https://docs.python.org/3/library/os.html) 
def display_class_hierarchy(classes: Union[Type, List[Type]], *,
                            public_methods: Optional[List] = None,
                            abstract_classes: Optional[List] = None,
                            include_methods: bool = True,
                            include_class_vars: bool = True,
                            include_legend: bool = True,
                            local_defs_only: bool = True,
                            types: Dict[str, Any] = {},
                            project: str = 'fuzzingbook',
                            log: bool = False) -> Any:
  """Visualize a class hierarchy.
`classes` is a Python class (or a list of classes) to be visualized.
`public_methods`, if given, is a list of methods to be shown as "public" (bold).
 (Default: all methods with a docstring)
`abstract_classes`, if given, is a list of classes to be shown as "abstract" (cursive).
 (Default: all classes with an abstract method)
`include_methods`: if set (default), include all methods
`include_legend`: if set (default), include a legend
`local_defs_only`: if set (default), hide details of imported classes
`types`: type names with definitions, to be used in docs
 """
    from [graphviz](https://graphviz.readthedocs.io/) import Digraph

    if project == 'debuggingbook':
        CLASS_FONT = 'Raleway, Helvetica, Arial, sans-serif'
        CLASS_COLOR = '#6A0DAD'  # HTML 'purple'
    else:
        CLASS_FONT = 'Patua One, Helvetica, sans-serif'
        CLASS_COLOR = '#B03A2E'

    METHOD_FONT = "'Fira Mono', 'Source Code Pro', 'Courier', monospace"
    METHOD_COLOR = 'black'

    if isinstance(classes, list):
        starting_class = classes[0]
    else:
        starting_class = classes
        classes = [starting_class]

    title = starting_class.__name__ + " class hierarchy"

    dot = Digraph(comment=title)
    dot.attr('node', shape='record', fontname=CLASS_FONT)
    dot.attr('graph', rankdir='BT', tooltip=title)
    dot.attr('edge', arrowhead='empty')

    # Hack to force rendering as HTML, allowing hovers and links in Jupyter
    dot._repr_html_ = dot._repr_image_svg_xml

    edges = set()
    overloaded_methods: Set[str] = set()

    drawn_classes = set()

    def method_string(method_name: str, public: bool, overloaded: bool,
                      fontsize: float = 10.0) -> str:
        method_string = f'<font face="{METHOD_FONT}" point-size="{str(fontsize)}">'

        if overloaded:
            name = f'<i>{method_name}()</i>'
        else:
            name = f'{method_name}()'

        if public:
            method_string += f'<b>{name}</b>'
        else:
            method_string += f'<font color="{METHOD_COLOR}">' \
                             f'{name}</font>'

        method_string += '</font>'
        return method_string

    def var_string(var_name: str, fontsize: int = 10) -> str:
        var_string = f'<font face="{METHOD_FONT}" point-size="{str(fontsize)}">'
        var_string += f'{var_name}'
        var_string += '</font>'
        return var_string

    def is_overloaded(method_name: str, f: Any) -> bool:
        return (method_name in overloaded_methods or
                (docstring(f) is not None and "in subclasses" in docstring(f)))

    def is_abstract(cls: Type) -> bool:
        if not abstract_classes:
            return inspect.isabstract(cls)

        return (cls in abstract_classes or
                any(c.__name__ == cls.__name__ for c in abstract_classes))

    def is_public(method_name: str, f: Any) -> bool:
        if public_methods:
            return (method_name in public_methods or
                    f in public_methods or
                    any(f.__qualname__ == m.__qualname__
                        for m in public_methods))

        return bool(docstring(f))

    def frame_module(frameinfo: Any) -> str:
        return os.path.splitext(os.path.basename(frameinfo.frame.f_code.co_filename))[0]

    def callers() -> List[str]:
        frames = inspect.getouterframes(inspect.currentframe())
        return [frame_module(frameinfo) for frameinfo in frames]

    def is_local_class(cls: Type) -> bool:
        return cls.__module__ == '__main__' or cls.__module__ in callers()

    def class_vars_string(cls: Type, url: str) -> str:
        cls_vars = class_vars(cls)
        if len(cls_vars) == 0:
            return ""

        vars_string = f'<table border="0" cellpadding="0" ' \
                      f'cellspacing="0" ' \
                      f'align="left" tooltip="{cls.__name__}" href="#">'

        for (name, var) in cls_vars:
            if log:
                print(f"    Drawing {name}")

            var_doc = escape(f"{name} = {repr(var)}")
            tooltip = f' tooltip="{var_doc}"'
            href = f' href="{url}"'
            vars_string += f'<tr><td align="left" border="0"' \
                           f'{tooltip}{href}>'

            vars_string += var_string(name)
            vars_string += '</td></tr>'

        vars_string += '</table>'
        return vars_string

    def class_methods_string(cls: Type, url: str) -> str:
        methods = public_class_methods(cls)
        # return "<br/>".join([name + "()" for (name, f) in methods])
        methods_string = f'<table border="0" cellpadding="0" ' \
                         f'cellspacing="0" ' \
                         f'align="left" tooltip="{cls.__name__}" href="#">'

        public_methods_only = local_defs_only and not is_local_class(cls)

        methods_seen = False
        for public in [True, False]:
            for (name, f) in methods:
                if public != is_public(name, f):
                    continue

                if public_methods_only and not public:
                    continue

                if log:
                    print(f"    Drawing {name}()")

                if is_public(name, f) and not docstring(f):
                    warnings.warn(f"{f.__qualname__}() is listed as public,"
                                  f" but has no docstring")

                overloaded = is_overloaded(name, f)

                sig = str(inspect.signature(f))
                # replace 'List[Union[...]]' by the actual type def
                for tp in types:
                    tp_def = str(types[tp]).replace('typing.', '')
                    sig = sig.replace(tp_def, tp)
                sig = sig.replace('__main__.', '')

                method_doc = escape(name + sig)
                if docstring(f):
                    method_doc += ":&#x0a;" + escape_doc(docstring(f))

                if log:
                    print(f"    Method doc: {method_doc}")

                # Tooltips are only shown if a href is present, too
                tooltip = f' tooltip="{method_doc}"'
                href = f' href="{url}"'
                methods_string += f'<tr><td align="left" border="0"' \
                                  f'{tooltip}{href}>'

                methods_string += method_string(name, public, overloaded)

                methods_string += '</td></tr>'
                methods_seen = True

        if not methods_seen:
            return ""

        methods_string += '</table>'
        return methods_string

    def display_class_node(cls: Type) -> None:
        name = cls.__name__

        if name in drawn_classes:
            return
        drawn_classes.add(name)

        if log:
            print(f"Drawing class {name}")

        if cls.__module__ == '__main__':
            url = '#'
        else:
            url = cls.__module__ + '.ipynb'

        if is_abstract(cls):
            formatted_class_name = f'<i>{cls.__name__}</i>'
        else:
            formatted_class_name = cls.__name__

        if include_methods or include_class_vars:
            vars = class_vars_string(cls, url)
            methods = class_methods_string(cls, url)
            spec = '<{<b><font color="' + CLASS_COLOR + '">' + \
                formatted_class_name + '</font></b>'
            if include_class_vars and vars:
                spec += '|' + vars
            if include_methods and methods:
                spec += '|' + methods
            spec += '}>'
        else:
            spec = '<' + formatted_class_name + '>'

        class_doc = escape('class ' + cls.__name__)
        if docstring(cls):
            class_doc += ':&#x0a;' + escape_doc(docstring(cls))
        else:
            warnings.warn(f"Class {cls.__name__} has no docstring")

        dot.node(name, spec, tooltip=class_doc, href=url)

    def display_class_trees(trees: List[Tuple[Type, List]]) -> None:
        for tree in trees:
            (cls, subtrees) = tree
            display_class_node(cls)

            for subtree in subtrees:
                (subcls, _) = subtree

                if (cls.__name__, subcls.__name__) not in edges:
                    dot.edge(cls.__name__, subcls.__name__)
                    edges.add((cls.__name__, subcls.__name__))

            display_class_trees(subtrees)

    def display_legend() -> None:
        fontsize = 8.0

        label = f'<b><font color="{CLASS_COLOR}">Legend</font></b><br align="left"/>' 

        for item in [
            method_string("public_method",
                          public=True, overloaded=False, fontsize=fontsize),
            method_string("private_method",
                          public=False, overloaded=False, fontsize=fontsize),
            method_string("overloaded_method",
                          public=False, overloaded=True, fontsize=fontsize)
        ]:
            label += '&bull;&nbsp;' + item + '<br align="left"/>'

        label += f'<font face="Helvetica" point-size="{str(fontsize  +  1)}">' \
                 'Hover over names to see doc' \
                 '</font><br align="left"/>'

        dot.node('Legend', label=f'<{label}>', shape='plain', fontsize=str(fontsize + 2))

    for cls in classes:
        tree = class_tree(cls)
        overloaded_methods = overloaded_class_methods(cls)
        display_class_trees(tree)

    if include_legend:
        display_legend()

    return dot 
display_class_hierarchy(D_Class, types={'SomeType': SomeType},
                        project='debuggingbook', log=True) 
Drawing class D_Class
    Drawing foo()
    Method doc: foo(self) -&gt; None:&#x0a;A WW2 foo fighter.
Drawing class B_Class
    Drawing VAR
    Drawing bar()
    Method doc: bar(self, qux: Any = None, bartender: int = 42) -&gt; None:&#x0a;A qux walks into a bar.&#x0a;`bartender` is an optional attribute.
    Drawing foo()
    Method doc: foo(self) -&gt; None:&#x0a;A WW2 foo fighter.
Drawing class A_Class
    Drawing foo()
    Method doc: foo(self) -&gt; None:&#x0a;The Adventures of the glorious Foo
    Drawing quux()
    Method doc: quux(self) -&gt; None:&#x0a;A method that is not used.
    Drawing second()
    Method doc: second(self) -&gt; None
Drawing class C_Class
    Drawing qux()
    Method doc: qux(self, arg: SomeType) -&gt; SomeType

D_Class <a xlink:href="#" xlink:title="class D_Class:

从多个超类继承的子类。

伴随相当长但无意义的文档。">D_Class <a xlink:href="#" xlink:title="foo(self) -> None:

一架二战时期的 foo 战斗机。">foo() B_Class <a xlink:href="#" xlink:title="class B_Class:

继承了一些方法的子类。《B_Class》 VAR <a xlink:href="#" xlink:title="bar(self, qux: Any = None, bartender: int = 42) -> None:

一个 qux 走进酒吧。

bartender是一个可选属性。">bar() <a xlink:href="#" xlink:title="foo(self) -> None:

一架二战时期的 foo 战斗机。">foo() D_Class->B_Class C_Class <a xlink:href="#" xlink:title="class C_Class:

注入了一些方法的类。《C_Class》 qux() D_Class->C_Class A_Class <a xlink:href="#" xlink:title="class A_Class:

一个正确完成 A 任务的类。

带有更长的文档字符串。">A_Class <a xlink:href="#" xlink:title="foo(self) -> None:

The Adventures of the glorious Foo">foo() <a xlink:href="#" xlink:title="quux(self) -> None:

未使用的方法。">quux() second() B_Class->A_Class 图例 图例 •  public_method() •  private_method() •  overloaded_method() 将鼠标悬停在名称上以查看文档

display_class_hierarchy(D_Class, types={'SomeType': SomeType},
                        project='fuzzingbook') 

D_Class <a xlink:href="#" xlink:title="class D_Class:

A subclass inheriting from multiple superclasses.

Comes with a fairly long, but meaningless documentation.">D_Class <a xlink:href="#" xlink:title="foo(self) -> None:

一架二战时期的 foo 战斗机。">foo() B_Class <a xlink:href="#" xlink:title="class B_Class:

继承一些方法的子类。《B_Class VAR <a xlink:href="#" xlink:title="bar(self, qux: Any = None, bartender: int = 42) -> None:

一只 qux 走进酒吧。《bar()`

bartender 是一个可选属性。">bar() <a xlink:href="#" xlink:title="foo(self) -> None:

一架二战时期的 foo 战斗机。">foo() D_Class->B_Class C_Class <a xlink:href="#" xlink:title="class C_Class:

向类中注入一些方法。《C_Class qux() D_Class->C_Class A_Class <a xlink:href="#" xlink:title="class A_Class:

正确完成 A 任务的类。

带有更长的文档字符串。">A_Class <a xlink:href="#" xlink:title="foo(self) -> None:

荣耀的 Foo 的冒险故事">foo() <a xlink:href="#" xlink:title="quux(self) -> None:

未使用的方 法。">quux() second() B_Class->A_Class 图例 图例 •  公共方法() •  私有方法() •  重载方法() 将鼠标悬停在名称上以查看文档

这里是一个带有抽象类和日志记录的变体:

display_class_hierarchy([A_Class, B_Class],
                        abstract_classes=[A_Class],
                        public_methods=[
                            A_Class.quux,
                        ],
                        log=True) 
Drawing class A_Class
    Drawing quux()
    Method doc: quux(self) -&gt; None:&#x0a;A method that is not used.
    Drawing foo()
    Method doc: foo(self) -&gt; None:&#x0a;The Adventures of the glorious Foo
    Drawing second()
    Method doc: second(self) -&gt; None
Drawing class B_Class
    Drawing VAR
    Drawing bar()
    Method doc: bar(self, qux: Any = None, bartender: int = 42) -&gt; None:&#x0a;A qux walks into a bar.&#x0a;`bartender` is an optional attribute.
    Drawing foo()
    Method doc: foo(self) -&gt; None:&#x0a;A WW2 foo fighter.

A_Class <a xlink:href="#" xlink:title="class A_Class:

正确执行某事的类。

附带更长的文档字符串。">A_Class <a xlink:href="#" xlink:title="quux(self) -> None:

一个未使用的函数。">quux() <a xlink:href="#" xlink:title="foo(self) -> None:

光荣的 Foo 的冒险故事">foo() second() B 类 <a xlink:href="#" xlink:title="class B 类:

继承了一些方法的子类。">B 类 VAR <a xlink:href="#" xlink:title="bar(self, qux: Any = None, bartender: int = 42) -> None:

一个 qux 走进了一家酒吧。

bartender是一个可选属性。">bar() <a xlink:href="#" xlink:title="foo(self) -> None:

一架二战时期的 foo 战斗机。">foo() B_Class->A_Class 图例 图例 •  public_method() •  private_method() •  overloaded_method() 将鼠标悬停在名称上以查看文档

练习

享受阅读!

Creative Commons License 本项目的内容受 Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议 的许可。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码受 MIT 许可协议 的许可。 最后更改:2024-06-30 18:45:02+02:00 • 引用 • 版权信息

如何引用这篇作品

Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, 和 Christian Holler: "类图". 在 Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, 和 Christian Holler 编著的 "模糊测试书籍", www.fuzzingbook.org/html/ClassDiagram.html. Retrieved 2024-06-30 18:45:02+02:00.

@incollection{fuzzingbook2024:ClassDiagram,
    author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
    booktitle = {The Fuzzing Book},
    title = {Class Diagrams},
    year = {2024},
    publisher = {CISPA Helmholtz Center for Information Security},
    howpublished = {\url{https://www.fuzzingbook.org/html/ClassDiagram.html}},
    note = {Retrieved 2024-06-30 18:45:02+02:00},
    url = {https://www.fuzzingbook.org/html/ClassDiagram.html},
    urldate = {2024-06-30 18:45:02+02:00}
}

铁路图

原文:www.fuzzingbook.org/html/RailroadDiagrams.html

这个笔记本中的代码有助于绘制语法图。它是 Tab Atkins jr. 的优秀库(略有定制)的副本,遗憾的是,这个库不是一个 Python 包。

先决条件

这个笔记本需要一些对 Python 和图形的高级概念的理解,特别是

* classes
* the Python `with` statement
* Scalable Vector Graphics
铁路图实现
import [bookutils.setup](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) 
import [re](https://docs.python.org/3/library/re.html)
import [io](https://docs.python.org/3/library/io.html) 
class C:
    # Display constants
    DEBUG = False  # if true, writes some debug information into attributes
    VS = 8  # minimum vertical separation between things. For a 3px stroke, must be at least 4
    AR = 10  # radius of arcs
    DIAGRAM_CLASS = 'railroad-diagram'  # class to put on the root <svg>
    # is the stroke width an odd (1px, 3px, etc) pixel length?
    STROKE_ODD_PIXEL_LENGTH = True
    # how to align items when they have extra space. left/right/center
    INTERNAL_ALIGNMENT = 'center'
    # width of each monospace character. play until you find the right value
    # for your font
    CHAR_WIDTH = 8.5
    COMMENT_CHAR_WIDTH = 7  # comments are in smaller text by default

    DEFAULT_STYLE = '''\
 svg.railroad-diagram {
 }
 svg.railroad-diagram path {
 stroke-width:3;
 stroke:black;
 fill:white;
 }
 svg.railroad-diagram text {
 font:14px "Fira Mono", monospace;
 text-anchor:middle;
 }
 svg.railroad-diagram text.label{
 text-anchor:start;
 }
 svg.railroad-diagram text.comment{
 font:italic 12px "Fira Mono", monospace;
 }
 svg.railroad-diagram rect{
 stroke-width:2;
 stroke:black;
 fill:mistyrose;
 }
''' 
def e(text):
    text = re.sub(r"&", '&amp;', str(text))
    text = re.sub(r"<", '&lt;', str(text))
    text = re.sub(r">", '&gt;', str(text))
    return str(text) 
def determineGaps(outer, inner):
    diff = outer - inner
    if C.INTERNAL_ALIGNMENT == 'left':
        return 0, diff
    elif C.INTERNAL_ALIGNMENT == 'right':
        return diff, 0
    else:
        return diff / 2, diff / 2 
def doubleenumerate(seq):
    length = len(list(seq))
    for i, item in enumerate(seq):
        yield i, i - length, item 
def addDebug(el):
    if not C.DEBUG:
        return
    el.attrs['data-x'] = "{0} w:{1} h:{2}/{3}/{4}".format(
        type(el).__name__, el.width, el.up, el.height, el.down) 
class DiagramItem:
    def __init__(self, name, attrs=None, text=None):
        self.name = name
        # up = distance it projects above the entry line
        # height = distance between the entry/exit lines
        # down = distance it projects below the exit line
        self.height = 0
        self.attrs = attrs or {}
        self.children = [text] if text else []
        self.needsSpace = False

    def format(self, x, y, width):
        raise NotImplementedError  # Virtual

    def addTo(self, parent):
        parent.children.append(self)
        return self

    def writeSvg(self, write):
        write(u'<{0}'.format(self.name))
        for name, value in sorted(self.attrs.items()):
            write(u' {0}="{1}"'.format(name, e(value)))
        write(u'>')
        if self.name in ["g", "svg"]:
            write(u'\n')
        for child in self.children:
            if isinstance(child, DiagramItem):
                child.writeSvg(write)
            else:
                write(e(child))
        write(u'</{0}>'.format(self.name))

    def __eq__(self, other):
        return isinstance(self, type(
            other)) and self.__dict__ == other.__dict__

    def __ne__(self, other):
        return not (self == other) 
class Path(DiagramItem):
    def __init__(self, x, y):
        self.x = x
        self.y = y
        DiagramItem.__init__(self, 'path', {'d': 'M%s  %s' % (x, y)})

    def m(self, x, y):
        self.attrs['d'] += 'm{0}  {1}'.format(x, y)
        return self

    def ll(self, x, y):   # was l(), which violates PEP8 -- AZ
        self.attrs['d'] += 'l{0}  {1}'.format(x, y)
        return self

    def h(self, val):
        self.attrs['d'] += 'h{0}'.format(val)
        return self

    def right(self, val):
        return self.h(max(0, val))

    def left(self, val):
        return self.h(-max(0, val))

    def v(self, val):
        self.attrs['d'] += 'v{0}'.format(val)
        return self

    def down(self, val):
        return self.v(max(0, val))

    def up(self, val):
        return self.v(-max(0, val))

    def arc_8(self, start, dir):
        # 1/8 of a circle
        arc = C.AR
        s2 = 1 / math.sqrt(2) * arc
        s2inv = (arc - s2)
        path = "a {0}  {0} 0 0 {1} ".format(arc, "1" if dir == 'cw' else "0")
        sd = start + dir
        if sd == 'ncw':
            offset = [s2, s2inv]
        elif sd == 'necw':
            offset = [s2inv, s2]
        elif sd == 'ecw':
            offset = [-s2inv, s2]
        elif sd == 'secw':
            offset = [-s2, s2inv]
        elif sd == 'scw':
            offset = [-s2, -s2inv]
        elif sd == 'swcw':
            offset = [-s2inv, -s2]
        elif sd == 'wcw':
            offset = [s2inv, -s2]
        elif sd == 'nwcw':
            offset = [s2, -s2inv]
        elif sd == 'nccw':
            offset = [-s2, s2inv]
        elif sd == 'nwccw':
            offset = [-s2inv, s2]
        elif sd == 'wccw':
            offset = [s2inv, s2]
        elif sd == 'swccw':
            offset = [s2, s2inv]
        elif sd == 'sccw':
            offset = [s2, -s2inv]
        elif sd == 'seccw':
            offset = [s2inv, -s2]
        elif sd == 'eccw':
            offset = [-s2inv, -s2]
        elif sd == 'neccw':
            offset = [-s2, -s2inv]

        path += " ".join(str(x) for x in offset)
        self.attrs['d'] += path
        return self

    def arc(self, sweep):
        x = C.AR
        y = C.AR
        if sweep[0] == 'e' or sweep[1] == 'w':
            x *= -1
        if sweep[0] == 's' or sweep[1] == 'n':
            y *= -1
        cw = 1 if sweep == 'ne' or sweep == 'es' or sweep == 'sw' or sweep == 'wn' else 0
        self.attrs['d'] += 'a{0}  {0} 0 0 {1}  {2}  {3}'.format(C.AR, cw, x, y)
        return self

    def format(self):
        self.attrs['d'] += 'h.5'
        return self

    def __repr__(self):
        return 'Path(%r, %r)' % (self.x, self.y) 
def wrapString(value):
    return value if isinstance(value, DiagramItem) else Terminal(value) 
class Style(DiagramItem):
    def __init__(self, css):
        self.name = 'style'
        self.css = css
        self.height = 0
        self.width = 0
        self.needsSpace = False

    def __repr__(self):
        return 'Style(%r)' % css

    def format(self, x, y, width):
        return self

    def writeSvg(self, write):
        # Write included stylesheet as CDATA. See
        # https:#developer.mozilla.org/en-US/docs/Web/SVG/Element/style
        cdata = u'/* <![CDATA[ */\n{css}\n/* ]]> */\n'.format(css=self.css)
        write(u'<style>{cdata}</style>'.format(cdata=cdata)) 
class Diagram(DiagramItem):
    def __init__(self, *items, **kwargs):
        # Accepts a type=[simple|complex] kwarg
        DiagramItem.__init__(
            self, 'svg', {'class': C.DIAGRAM_CLASS, 'xmlns': "http://www.w3.org/2000/svg"})
        self.type = kwargs.get("type", "simple")
        self.items = [wrapString(item) for item in items]
        if items and not isinstance(items[0], Start):
            self.items.insert(0, Start(self.type))
        if items and not isinstance(items[-1], End):
            self.items.append(End(self.type))
        self.css = kwargs.get("css", C.DEFAULT_STYLE)
        if self.css:
            self.items.insert(0, Style(self.css))
        self.up = 0
        self.down = 0
        self.height = 0
        self.width = 0
        for item in self.items:
            if isinstance(item, Style):
                continue
            self.width += item.width + (20 if item.needsSpace else 0)
            self.up = max(self.up, item.up - self.height)
            self.height += item.height
            self.down = max(self.down - item.height, item.down)
        if self.items[0].needsSpace:
            self.width -= 10
        if self.items[-1].needsSpace:
            self.width -= 10
        self.formatted = False

    def __repr__(self):
        if self.css:
            items = ', '.join(map(repr, self.items[2:-1]))
        else:
            items = ', '.join(map(repr, self.items[1:-1]))
        pieces = [] if not items else [items]
        if self.css != C.DEFAULT_STYLE:
            pieces.append('css=%r' % self.css)
        if self.type != 'simple':
            pieces.append('type=%r' % self.type)
        return 'Diagram(%s)' % ', '.join(pieces)

    def format(self, paddingTop=20, paddingRight=None,
               paddingBottom=None, paddingLeft=None):
        if paddingRight is None:
            paddingRight = paddingTop
        if paddingBottom is None:
            paddingBottom = paddingTop
        if paddingLeft is None:
            paddingLeft = paddingRight
        x = paddingLeft
        y = paddingTop + self.up
        g = DiagramItem('g')
        if C.STROKE_ODD_PIXEL_LENGTH:
            g.attrs['transform'] = 'translate(.5 .5)'
        for item in self.items:
            if item.needsSpace:
                Path(x, y).h(10).addTo(g)
                x += 10
            item.format(x, y, item.width).addTo(g)
            x += item.width
            y += item.height
            if item.needsSpace:
                Path(x, y).h(10).addTo(g)
                x += 10
        self.attrs['width'] = self.width + paddingLeft + paddingRight
        self.attrs['height'] = self.up + self.height + \
            self.down + paddingTop + paddingBottom
        self.attrs['viewBox'] = "0 0 {width}  {height}".format(**self.attrs)
        g.addTo(self)
        self.formatted = True
        return self

    def writeSvg(self, write):
        if not self.formatted:
            self.format()
        return DiagramItem.writeSvg(self, write)

    def parseCSSGrammar(self, text):
        token_patterns = {
            'keyword': r"[\w-]+\(?",
            'type': r"<[\w-]+(\(\))?>",
            'char': r"[/,()]",
            'literal': r"'(.)'",
            'openbracket': r"\[",
            'closebracket': r"\]",
            'closebracketbang': r"\]!",
            'bar': r"\|",
            'doublebar': r"\|\|",
            'doubleand': r"&&",
            'multstar': r"\*",
            'multplus': r"\+",
            'multhash': r"#",
            'multnum1': r"{\s*(\d+)\s*}",
            'multnum2': r"{\s*(\d+)\s*,\s*(\d*)\s*}",
            'multhashnum1': r"#{\s*(\d+)\s*}",
            'multhashnum2': r"{\s*(\d+)\s*,\s*(\d*)\s*}"
        } 
class Sequence(DiagramItem):
    def __init__(self, *items):
        DiagramItem.__init__(self, 'g')
        self.items = [wrapString(item) for item in items]
        self.needsSpace = True
        self.up = 0
        self.down = 0
        self.height = 0
        self.width = 0
        for item in self.items:
            self.width += item.width + (20 if item.needsSpace else 0)
            self.up = max(self.up, item.up - self.height)
            self.height += item.height
            self.down = max(self.down - item.height, item.down)
        if self.items[0].needsSpace:
            self.width -= 10
        if self.items[-1].needsSpace:
            self.width -= 10
        addDebug(self)

    def __repr__(self):
        items = ', '.join(map(repr, self.items))
        return 'Sequence(%s)' % items

    def format(self, x, y, width):
        leftGap, rightGap = determineGaps(width, self.width)
        Path(x, y).h(leftGap).addTo(self)
        Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
        x += leftGap
        for i, item in enumerate(self.items):
            if item.needsSpace and i > 0:
                Path(x, y).h(10).addTo(self)
                x += 10
            item.format(x, y, item.width).addTo(self)
            x += item.width
            y += item.height
            if item.needsSpace and i < len(self.items) - 1:
                Path(x, y).h(10).addTo(self)
                x += 10
        return self 
class Stack(DiagramItem):
    def __init__(self, *items):
        DiagramItem.__init__(self, 'g')
        self.items = [wrapString(item) for item in items]
        self.needsSpace = True
        self.width = max(item.width + (20 if item.needsSpace else 0)
                         for item in self.items)
        # pretty sure that space calc is totes wrong
        if len(self.items) > 1:
            self.width += C.AR * 2
        self.up = self.items[0].up
        self.down = self.items[-1].down
        self.height = 0
        last = len(self.items) - 1
        for i, item in enumerate(self.items):
            self.height += item.height
            if i > 0:
                self.height += max(C.AR * 2, item.up + C.VS)
            if i < last:
                self.height += max(C.AR * 2, item.down + C.VS)
        addDebug(self)

    def __repr__(self):
        items = ', '.join(repr(item) for item in self.items)
        return 'Stack(%s)' % items

    def format(self, x, y, width):
        leftGap, rightGap = determineGaps(width, self.width)
        Path(x, y).h(leftGap).addTo(self)
        x += leftGap
        xInitial = x
        if len(self.items) > 1:
            Path(x, y).h(C.AR).addTo(self)
            x += C.AR
            innerWidth = self.width - C.AR * 2
        else:
            innerWidth = self.width
        for i, item in enumerate(self.items):
            item.format(x, y, innerWidth).addTo(self)
            x += innerWidth
            y += item.height
            if i != len(self.items) - 1:
                (Path(x, y)
                    .arc('ne').down(max(0, item.down + C.VS - C.AR * 2))
                    .arc('es').left(innerWidth)
                    .arc('nw').down(max(0, self.items[i + 1].up + C.VS - C.AR * 2))
                    .arc('ws').addTo(self))
                y += max(item.down + C.VS, C.AR * 2) + \
                    max(self.items[i + 1].up + C.VS, C.AR * 2)
                x = xInitial + C.AR
        if len(self.items) > 1:
            Path(x, y).h(C.AR).addTo(self)
            x += C.AR
        Path(x, y).h(rightGap).addTo(self)
        return self 
class OptionalSequence(DiagramItem):
    def __new__(cls, *items):
        if len(items) <= 1:
            return Sequence(*items)
        else:
            return super(OptionalSequence, cls).__new__(cls)

    def __init__(self, *items):
        DiagramItem.__init__(self, 'g')
        self.items = [wrapString(item) for item in items]
        self.needsSpace = False
        self.width = 0
        self.up = 0
        self.height = sum(item.height for item in self.items)
        self.down = self.items[0].down
        heightSoFar = 0
        for i, item in enumerate(self.items):
            self.up = max(self.up, max(C.AR * 2, item.up + C.VS) - heightSoFar)
            heightSoFar += item.height
            if i > 0:
                self.down = max(self.height + self.down, heightSoFar
                                + max(C.AR * 2, item.down + C.VS)) - self.height
            itemWidth = item.width + (20 if item.needsSpace else 0)
            if i == 0:
                self.width += C.AR + max(itemWidth, C.AR)
            else:
                self.width += C.AR * 2 + max(itemWidth, C.AR) + C.AR
        addDebug(self)

    def __repr__(self):
        items = ', '.join(repr(item) for item in self.items)
        return 'OptionalSequence(%s)' % items

    def format(self, x, y, width):
        leftGap, rightGap = determineGaps(width, self.width)
        Path(x, y).right(leftGap).addTo(self)
        Path(x + leftGap + self.width, y
             + self.height).right(rightGap).addTo(self)
        x += leftGap
        upperLineY = y - self.up
        last = len(self.items) - 1
        for i, item in enumerate(self.items):
            itemSpace = 10 if item.needsSpace else 0
            itemWidth = item.width + itemSpace
            if i == 0:
                # Upper skip
                (Path(x, y)
                    .arc('se')
                    .up(y - upperLineY - C.AR * 2)
                    .arc('wn')
                    .right(itemWidth - C.AR)
                    .arc('ne')
                    .down(y + item.height - upperLineY - C.AR * 2)
                    .arc('ws')
                    .addTo(self))
                # Straight line
                (Path(x, y)
                    .right(itemSpace + C.AR)
                    .addTo(self))
                item.format(x + itemSpace + C.AR, y, item.width).addTo(self)
                x += itemWidth + C.AR
                y += item.height
            elif i < last:
                # Upper skip
                (Path(x, upperLineY)
                    .right(C.AR * 2 + max(itemWidth, C.AR) + C.AR)
                    .arc('ne')
                    .down(y - upperLineY + item.height - C.AR * 2)
                    .arc('ws')
                    .addTo(self))
                # Straight line
                (Path(x, y)
                    .right(C.AR * 2)
                    .addTo(self))
                item.format(x + C.AR * 2, y, item.width).addTo(self)
                (Path(x + item.width + C.AR * 2, y + item.height)
                    .right(itemSpace + C.AR)
                    .addTo(self))
                # Lower skip
                (Path(x, y)
                    .arc('ne')
                    .down(item.height + max(item.down + C.VS, C.AR * 2) - C.AR * 2)
                    .arc('ws')
                    .right(itemWidth - C.AR)
                    .arc('se')
                    .up(item.down + C.VS - C.AR * 2)
                    .arc('wn')
                    .addTo(self))
                x += C.AR * 2 + max(itemWidth, C.AR) + C.AR
                y += item.height
            else:
                # Straight line
                (Path(x, y)
                    .right(C.AR * 2)
                    .addTo(self))
                item.format(x + C.AR * 2, y, item.width).addTo(self)
                (Path(x + C.AR * 2 + item.width, y + item.height)
                    .right(itemSpace + C.AR)
                    .addTo(self))
                # Lower skip
                (Path(x, y)
                    .arc('ne')
                    .down(item.height + max(item.down + C.VS, C.AR * 2) - C.AR * 2)
                    .arc('ws')
                    .right(itemWidth - C.AR)
                    .arc('se')
                    .up(item.down + C.VS - C.AR * 2)
                    .arc('wn')
                    .addTo(self))
        return self 
class AlternatingSequence(DiagramItem):
    def __new__(cls, *items):
        if len(items) == 2:
            return super(AlternatingSequence, cls).__new__(cls)
        else:
            raise Exception(
                "AlternatingSequence takes exactly two arguments got " + len(items))

    def __init__(self, *items):
        DiagramItem.__init__(self, 'g')
        self.items = [wrapString(item) for item in items]
        self.needsSpace = False

        arc = C.AR
        vert = C.VS
        first = self.items[0]
        second = self.items[1]

        arcX = 1 / math.sqrt(2) * arc * 2
        arcY = (1 - 1 / math.sqrt(2)) * arc * 2
        crossY = max(arc, vert)
        crossX = (crossY - arcY) + arcX

        firstOut = max(arc + arc, crossY / 2 + arc + arc,
                       crossY / 2 + vert + first.down)
        self.up = firstOut + first.height + first.up

        secondIn = max(arc + arc, crossY / 2 + arc + arc,
                       crossY / 2 + vert + second.up)
        self.down = secondIn + second.height + second.down

        self.height = 0

        firstWidth = (20 if first.needsSpace else 0) + first.width
        secondWidth = (20 if second.needsSpace else 0) + second.width
        self.width = 2 * arc + max(firstWidth, crossX, secondWidth) + 2 * arc
        addDebug(self)

    def __repr__(self):
        items = ', '.join(repr(item) for item in self.items)
        return 'AlternatingSequence(%s)' % items

    def format(self, x, y, width):
        arc = C.AR
        gaps = determineGaps(width, self.width)
        Path(x, y).right(gaps[0]).addTo(self)
        x += gaps[0]
        Path(x + self.width, y).right(gaps[1]).addTo(self)
        # bounding box
        # Path(x+gaps[0], y).up(self.up).right(self.width).down(self.up+self.down).left(self.width).up(self.down).addTo(self)
        first = self.items[0]
        second = self.items[1]

        # top
        firstIn = self.up - first.up
        firstOut = self.up - first.up - first.height
        Path(x, y).arc('se').up(firstIn - 2 * arc).arc('wn').addTo(self)
        first.format(
            x
            + 2
            * arc,
            y
            - firstIn,
            self.width
            - 4
            * arc).addTo(self)
        Path(x + self.width - 2 * arc, y
             - firstOut).arc('ne').down(firstOut - 2 * arc).arc('ws').addTo(self)

        # bottom
        secondIn = self.down - second.down - second.height
        secondOut = self.down - second.down
        Path(x, y).arc('ne').down(secondIn - 2 * arc).arc('ws').addTo(self)
        second.format(
            x
            + 2
            * arc,
            y
            + secondIn,
            self.width
            - 4
            * arc).addTo(self)
        Path(x + self.width - 2 * arc, y
             + secondOut).arc('se').up(secondOut - 2 * arc).arc('wn').addTo(self)

        # crossover
        arcX = 1 / Math.sqrt(2) * arc * 2
        arcY = (1 - 1 / Math.sqrt(2)) * arc * 2
        crossY = max(arc, C.VS)
        crossX = (crossY - arcY) + arcX
        crossBar = (self.width - 4 * arc - crossX) / 2
        (Path(x + arc, y - crossY / 2 - arc).arc('ws').right(crossBar)
            .arc_8('n', 'cw').ll(crossX - arcX, crossY - arcY).arc_8('sw', 'ccw')
            .right(crossBar).arc('ne').addTo(self))
        (Path(x + arc, y + crossY / 2 + arc).arc('wn').right(crossBar)
            .arc_8('s', 'ccw').ll(crossX - arcX, -(crossY - arcY)).arc_8('nw', 'cw')
            .right(crossBar).arc('se').addTo(self))

        return self 
class Choice(DiagramItem):
    def __init__(self, default, *items):
        DiagramItem.__init__(self, 'g')
        assert default < len(items)
        self.default = default
        self.items = [wrapString(item) for item in items]
        self.width = C.AR * 4 + max(item.width for item in self.items)
        self.up = self.items[0].up
        self.down = self.items[-1].down
        self.height = self.items[default].height
        for i, item in enumerate(self.items):
            if i in [default - 1, default + 1]:
                arcs = C.AR * 2
            else:
                arcs = C.AR
            if i < default:
                self.up += max(arcs, item.height + item.down
                               + C.VS + self.items[i + 1].up)
            elif i == default:
                continue
            else:
                self.down += max(arcs, item.up + C.VS
                                 + self.items[i - 1].down + self.items[i - 1].height)
        # already counted in self.height
        self.down -= self.items[default].height
        addDebug(self)

    def __repr__(self):
        items = ', '.join(repr(item) for item in self.items)
        return 'Choice(%r, %s)' % (self.default, items)

    def format(self, x, y, width):
        leftGap, rightGap = determineGaps(width, self.width)

        # Hook up the two sides if self is narrower than its stated width.
        Path(x, y).h(leftGap).addTo(self)
        Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
        x += leftGap

        innerWidth = self.width - C.AR * 4
        default = self.items[self.default]

        # Do the elements that curve above
        above = self.items[:self.default][::-1]
        if above:
            distanceFromY = max(
                C.AR * 2,
                default.up
                + C.VS
                + above[0].down
                + above[0].height)
        for i, ni, item in doubleenumerate(above):
            Path(x, y).arc('se').up(distanceFromY
                                    - C.AR * 2).arc('wn').addTo(self)
            item.format(x + C.AR * 2, y - distanceFromY,
                        innerWidth).addTo(self)
            Path(x + C.AR * 2 + innerWidth, y - distanceFromY + item.height).arc('ne') \
                .down(distanceFromY - item.height + default.height - C.AR * 2).arc('ws').addTo(self)
            if ni < -1:
                distanceFromY += max(
                    C.AR,
                    item.up
                    + C.VS
                    + above[i + 1].down
                    + above[i + 1].height)

        # Do the straight-line path.
        Path(x, y).right(C.AR * 2).addTo(self)
        self.items[self.default].format(
            x + C.AR * 2, y, innerWidth).addTo(self)
        Path(x + C.AR * 2 + innerWidth, y
             + self.height).right(C.AR * 2).addTo(self)

        # Do the elements that curve below
        below = self.items[self.default + 1:]
        if below:
            distanceFromY = max(
                C.AR * 2,
                default.height
                + default.down
                + C.VS
                + below[0].up)
        for i, item in enumerate(below):
            Path(x, y).arc('ne').down(
                distanceFromY - C.AR * 2).arc('ws').addTo(self)
            item.format(x + C.AR * 2, y + distanceFromY,
                        innerWidth).addTo(self)
            Path(x + C.AR * 2 + innerWidth, y + distanceFromY + item.height).arc('se') \
                .up(distanceFromY - C.AR * 2 + item.height - default.height).arc('wn').addTo(self)
            distanceFromY += max(
                C.AR,
                item.height
                + item.down
                + C.VS
                + (below[i + 1].up if i + 1 < len(below) else 0))
        return self 
class MultipleChoice(DiagramItem):
    def __init__(self, default, type, *items):
        DiagramItem.__init__(self, 'g')
        assert 0 <= default < len(items)
        assert type in ["any", "all"]
        self.default = default
        self.type = type
        self.needsSpace = True
        self.items = [wrapString(item) for item in items]
        self.innerWidth = max(item.width for item in self.items)
        self.width = 30 + C.AR + self.innerWidth + C.AR + 20
        self.up = self.items[0].up
        self.down = self.items[-1].down
        self.height = self.items[default].height
        for i, item in enumerate(self.items):
            if i in [default - 1, default + 1]:
                minimum = 10 + C.AR
            else:
                minimum = C.AR
            if i < default:
                self.up += max(minimum, item.height
                               + item.down + C.VS + self.items[i + 1].up)
            elif i == default:
                continue
            else:
                self.down += max(minimum, item.up + C.VS
                                 + self.items[i - 1].down + self.items[i - 1].height)
        # already counted in self.height
        self.down -= self.items[default].height
        addDebug(self)

    def __repr__(self):
        items = ', '.join(map(repr, self.items))
        return 'MultipleChoice(%r, %r, %s)' % (self.default, self.type, items)

    def format(self, x, y, width):
        leftGap, rightGap = determineGaps(width, self.width)

        # Hook up the two sides if self is narrower than its stated width.
        Path(x, y).h(leftGap).addTo(self)
        Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
        x += leftGap

        default = self.items[self.default]

        # Do the elements that curve above
        above = self.items[:self.default][::-1]
        if above:
            distanceFromY = max(
                10 + C.AR,
                default.up
                + C.VS
                + above[0].down
                + above[0].height)
        for i, ni, item in doubleenumerate(above):
            (Path(x + 30, y)
                .up(distanceFromY - C.AR)
                .arc('wn')
                .addTo(self))
            item.format(x + 30 + C.AR, y - distanceFromY,
                        self.innerWidth).addTo(self)
            (Path(x + 30 + C.AR + self.innerWidth, y - distanceFromY + item.height)
                .arc('ne')
                .down(distanceFromY - item.height + default.height - C.AR - 10)
                .addTo(self))
            if ni < -1:
                distanceFromY += max(
                    C.AR,
                    item.up
                    + C.VS
                    + above[i + 1].down
                    + above[i + 1].height)

        # Do the straight-line path.
        Path(x + 30, y).right(C.AR).addTo(self)
        self.items[self.default].format(
            x + 30 + C.AR, y, self.innerWidth).addTo(self)
        Path(x + 30 + C.AR + self.innerWidth, y
             + self.height).right(C.AR).addTo(self)

        # Do the elements that curve below
        below = self.items[self.default + 1:]
        if below:
            distanceFromY = max(
                10 + C.AR,
                default.height
                + default.down
                + C.VS
                + below[0].up)
        for i, item in enumerate(below):
            (Path(x + 30, y)
                .down(distanceFromY - C.AR)
                .arc('ws')
                .addTo(self))
            item.format(x + 30 + C.AR, y + distanceFromY,
                        self.innerWidth).addTo(self)
            (Path(x + 30 + C.AR + self.innerWidth, y + distanceFromY + item.height)
                .arc('se')
                .up(distanceFromY - C.AR + item.height - default.height - 10)
                .addTo(self))
            distanceFromY += max(
                C.AR,
                item.height
                + item.down
                + C.VS
                + (below[i + 1].up if i + 1 < len(below) else 0))
        text = DiagramItem('g', attrs={"class": "diagram-text"}).addTo(self)
        DiagramItem('title', text="take one or more branches, once each, in any order" if self.type
                    == "any" else "take all branches, once each, in any order").addTo(text)
        DiagramItem('path', attrs={
            "d": "M {x}  {y} h -26 a 4 4 0 0 0 -4 4 v 12 a 4 4 0 0 0 4 4 h 26 z".format(x=x + 30, y=y - 10),
            "class": "diagram-text"
        }).addTo(text)
        DiagramItem('text', text="1+" if self.type == "any" else "all", attrs={
            "x": x + 15,
            "y": y + 4,
            "class": "diagram-text"
        }).addTo(text)
        DiagramItem('path', attrs={
            "d": "M {x}  {y} h 16 a 4 4 0 0 1 4 4 v 12 a 4 4 0 0 1 -4 4 h -16 z".format(x=x + self.width - 20, y=y - 10),
            "class": "diagram-text"
        }).addTo(text)
        DiagramItem('text', text=u"↺", attrs={
            "x": x + self.width - 10,
            "y": y + 4,
            "class": "diagram-arrow"
        }).addTo(text)
        return self 
class HorizontalChoice(DiagramItem):
    def __new__(cls, *items):
        if len(items) <= 1:
            return Sequence(*items)
        else:
            return super(HorizontalChoice, cls).__new__(cls)

    def __init__(self, *items):
        DiagramItem.__init__(self, 'g')
        self.items = [wrapString(item) for item in items]
        allButLast = self.items[:-1]
        middles = self.items[1:-1]
        first = self.items[0]
        last = self.items[-1]
        self.needsSpace = False

        self.width = (C.AR  # starting track
                      + C.AR * 2 * (len(self.items) - 1)  # inbetween tracks
                      + sum(x.width + (20 if x.needsSpace else 0)
                            for x in self.items)  # items
                      # needs space to curve up
                      + (C.AR if last.height > 0 else 0)
                      + C.AR)  # ending track

        # Always exits at entrance height
        self.height = 0

        # All but the last have a track running above them
        self._upperTrack = max(
            C.AR * 2,
            C.VS,
            max(x.up for x in allButLast) + C.VS
        )
        self.up = max(self._upperTrack, last.up)

        # All but the first have a track running below them
        # Last either straight-lines or curves up, so has different calculation
        self._lowerTrack = max(
            C.VS,
            max(x.height + max(x.down + C.VS, C.AR * 2)
                for x in middles) if middles else 0,
            last.height + last.down + C.VS
        )
        if first.height < self._lowerTrack:
            # Make sure there's at least 2*C.AR room between first exit and
            # lower track
            self._lowerTrack = max(self._lowerTrack, first.height + C.AR * 2)
        self.down = max(self._lowerTrack, first.height + first.down)

        addDebug(self)

    def format(self, x, y, width):
        # Hook up the two sides if self is narrower than its stated width.
        leftGap, rightGap = determineGaps(width, self.width)
        Path(x, y).h(leftGap).addTo(self)
        Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
        x += leftGap

        first = self.items[0]
        last = self.items[-1]

        # upper track
        upperSpan = (sum(x.width + (20 if x.needsSpace else 0) for x in self.items[:-1])
                     + (len(self.items) - 2) * C.AR * 2
                     - C.AR)
        (Path(x, y)
            .arc('se')
            .up(self._upperTrack - C.AR * 2)
            .arc('wn')
            .h(upperSpan)
            .addTo(self))

        # lower track
        lowerSpan = (sum(x.width + (20 if x.needsSpace else 0) for x in self.items[1:])
                     + (len(self.items) - 2) * C.AR * 2
                     + (C.AR if last.height > 0 else 0)
                     - C.AR)
        lowerStart = x + C.AR + first.width + \
            (20 if first.needsSpace else 0) + C.AR * 2
        (Path(lowerStart, y + self._lowerTrack)
            .h(lowerSpan)
            .arc('se')
            .up(self._lowerTrack - C.AR * 2)
            .arc('wn')
            .addTo(self))

        # Items
        for [i, item] in enumerate(self.items):
            # input track
            if i == 0:
                (Path(x, y)
                    .h(C.AR)
                    .addTo(self))
                x += C.AR
            else:
                (Path(x, y - self._upperTrack)
                    .arc('ne')
                    .v(self._upperTrack - C.AR * 2)
                    .arc('ws')
                    .addTo(self))
                x += C.AR * 2

            # item
            itemWidth = item.width + (20 if item.needsSpace else 0)
            item.format(x, y, itemWidth).addTo(self)
            x += itemWidth

            # output track
            if i == len(self.items) - 1:
                if item.height == 0:
                    (Path(x, y)
                        .h(C.AR)
                        .addTo(self))
                else:
                    (Path(x, y + item.height)
                        .arc('se')
                        .addTo(self))
            elif i == 0 and item.height > self._lowerTrack:
                # Needs to arc up to meet the lower track, not down.
                if item.height - self._lowerTrack >= C.AR * 2:
                    (Path(x, y + item.height)
                        .arc('se')
                        .v(self._lowerTrack - item.height + C.AR * 2)
                        .arc('wn')
                        .addTo(self))
                else:
                    # Not enough space to fit two arcs
                    # so just bail and draw a straight line for now.
                    (Path(x, y + item.height)
                        .ll(C.AR * 2, self._lowerTrack - item.height)
                        .addTo(self))
            else:
                (Path(x, y + item.height)
                    .arc('ne')
                    .v(self._lowerTrack - item.height - C.AR * 2)
                    .arc('ws')
                    .addTo(self))
        return self 
def Optional(item, skip=False):
    return Choice(0 if skip else 1, Skip(), item) 
class OneOrMore(DiagramItem):
    def __init__(self, item, repeat=None):
        DiagramItem.__init__(self, 'g')
        repeat = repeat or Skip()
        self.item = wrapString(item)
        self.rep = wrapString(repeat)
        self.width = max(self.item.width, self.rep.width) + C.AR * 2
        self.height = self.item.height
        self.up = self.item.up
        self.down = max(
            C.AR * 2,
            self.item.down + C.VS + self.rep.up + self.rep.height + self.rep.down)
        self.needsSpace = True
        addDebug(self)

    def format(self, x, y, width):
        leftGap, rightGap = determineGaps(width, self.width)

        # Hook up the two sides if self is narrower than its stated width.
        Path(x, y).h(leftGap).addTo(self)
        Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
        x += leftGap

        # Draw item
        Path(x, y).right(C.AR).addTo(self)
        self.item.format(x + C.AR, y, self.width - C.AR * 2).addTo(self)
        Path(x + self.width - C.AR, y + self.height).right(C.AR).addTo(self)

        # Draw repeat arc
        distanceFromY = max(C.AR * 2, self.item.height
                            + self.item.down + C.VS + self.rep.up)
        Path(x + C.AR, y).arc('nw').down(distanceFromY - C.AR * 2) \
            .arc('ws').addTo(self)
        self.rep.format(x + C.AR, y + distanceFromY,
                        self.width - C.AR * 2).addTo(self)
        Path(x + self.width - C.AR, y + distanceFromY + self.rep.height).arc('se') \
            .up(distanceFromY - C.AR * 2 + self.rep.height - self.item.height).arc('en').addTo(self)

        return self

    def __repr__(self):
        return 'OneOrMore(%r, repeat=%r)' % (self.item, self.rep) 
def ZeroOrMore(item, repeat=None, skip=False):
    result = Optional(OneOrMore(item, repeat), skip)
    return result 
class Start(DiagramItem):
    def __init__(self, type="simple", label=None):
        DiagramItem.__init__(self, 'g')
        if label:
            self.width = max(20, len(label) * C.CHAR_WIDTH + 10)
        else:
            self.width = 20
        self.up = 10
        self.down = 10
        self.type = type
        self.label = label
        addDebug(self)

    def format(self, x, y, _width):
        path = Path(x, y - 10)
        if self.type == "complex":
            path.down(20).m(0, -10).right(self.width).addTo(self)
        else:
            path.down(20).m(10, -20).down(20).m(-10,
                                                - 10).right(self.width).addTo(self)
        if self.label:
            DiagramItem('text', attrs={
                        "x": x, "y": y - 15, "style": "text-anchor:start"}, text=self.label).addTo(self)
        return self

    def __repr__(self):
        return 'Start(type=%r, label=%r)' % (self.type, self.label) 
class End(DiagramItem):
    def __init__(self, type="simple"):
        DiagramItem.__init__(self, 'path')
        self.width = 20
        self.up = 10
        self.down = 10
        self.type = type
        addDebug(self)

    def format(self, x, y, _width):
        if self.type == "simple":
            self.attrs['d'] = 'M {0}  {1} h 20 m -10 -10 v 20 m 10 -20 v 20'.format(
                x, y)
        elif self.type == "complex":
            self.attrs['d'] = 'M {0}  {1} h 20 m 0 -10 v 20'
        return self

    def __repr__(self):
        return 'End(type=%r)' % self.type 
class Terminal(DiagramItem):
    def __init__(self, text, href=None, title=None):
        DiagramItem.__init__(self, 'g', {'class': 'terminal'})
        self.text = text
        self.href = href
        self.title = title
        self.width = len(text) * C.CHAR_WIDTH + 20
        self.up = 11
        self.down = 11
        self.needsSpace = True
        addDebug(self)

    def __repr__(self):
        return 'Terminal(%r, href=%r, title=%r)' % (
            self.text, self.href, self.title)

    def format(self, x, y, width):
        leftGap, rightGap = determineGaps(width, self.width)

        # Hook up the two sides if self is narrower than its stated width.
        Path(x, y).h(leftGap).addTo(self)
        Path(x + leftGap + self.width, y).h(rightGap).addTo(self)

        DiagramItem('rect', {'x': x + leftGap, 'y': y - 11, 'width': self.width,
                             'height': self.up + self.down, 'rx': 10, 'ry': 10}).addTo(self)
        text = DiagramItem('text', {'x': x + width / 2, 'y': y + 4}, self.text)
        if self.href is not None:
            a = DiagramItem('a', {'xlink:href': self.href}, text).addTo(self)
            text.addTo(a)
        else:
            text.addTo(self)
        if self.title is not None:
            DiagramItem('title', {}, self.title).addTo(self)
        return self 
class NonTerminal(DiagramItem):
    def __init__(self, text, href=None, title=None):
        DiagramItem.__init__(self, 'g', {'class': 'non-terminal'})
        self.text = text
        self.href = href
        self.title = title
        self.width = len(text) * C.CHAR_WIDTH + 20
        self.up = 11
        self.down = 11
        self.needsSpace = True
        addDebug(self)

    def __repr__(self):
        return 'NonTerminal(%r, href=%r, title=%r)' % (
            self.text, self.href, self.title)

    def format(self, x, y, width):
        leftGap, rightGap = determineGaps(width, self.width)

        # Hook up the two sides if self is narrower than its stated width.
        Path(x, y).h(leftGap).addTo(self)
        Path(x + leftGap + self.width, y).h(rightGap).addTo(self)

        DiagramItem('rect', {'x': x + leftGap, 'y': y - 11, 'width': self.width,
                             'height': self.up + self.down}).addTo(self)
        text = DiagramItem('text', {'x': x + width / 2, 'y': y + 4}, self.text)
        if self.href is not None:
            a = DiagramItem('a', {'xlink:href': self.href}, text).addTo(self)
            text.addTo(a)
        else:
            text.addTo(self)
        if self.title is not None:
            DiagramItem('title', {}, self.title).addTo(self)
        return self 
class Comment(DiagramItem):
    def __init__(self, text, href=None, title=None):
        DiagramItem.__init__(self, 'g')
        self.text = text
        self.href = href
        self.title = title
        self.width = len(text) * C.COMMENT_CHAR_WIDTH + 10
        self.up = 11
        self.down = 11
        self.needsSpace = True
        addDebug(self)

    def __repr__(self):
        return 'Comment(%r, href=%r, title=%r)' % (
            self.text, self.href, self.title)

    def format(self, x, y, width):
        leftGap, rightGap = determineGaps(width, self.width)

        # Hook up the two sides if self is narrower than its stated width.
        Path(x, y).h(leftGap).addTo(self)
        Path(x + leftGap + self.width, y).h(rightGap).addTo(self)

        text = DiagramItem(
            'text', {'x': x + width / 2, 'y': y + 5, 'class': 'comment'}, self.text)
        if self.href is not None:
            a = DiagramItem('a', {'xlink:href': self.href}, text).addTo(self)
            text.addTo(a)
        else:
            text.addTo(self)
        if self.title is not None:
            DiagramItem('title', {}, self.title).addTo(self)
        return self 
class Skip(DiagramItem):
    def __init__(self):
        DiagramItem.__init__(self, 'g')
        self.width = 0
        self.up = 0
        self.down = 0
        addDebug(self)

    def format(self, x, y, width):
        Path(x, y).right(width).addTo(self)
        return self

    def __repr__(self):
        return 'Skip()' 
def show_diagram(graph, log=False):
    with io.StringIO() as f:
        d = Diagram(graph)
        if log:
            print(d)
        d.writeSvg(f.write)
        mysvg = f.getvalue()
        return mysvg 
```</details>

![Creative Commons License](https://github.com/OpenDocCN/geekdoc-sec-zh/raw/master/docs/fzbk/img/2f3faa36146c6fb38bbab67add09aa5f.png) 本项目的内容受 [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc-sa/4.0/) 的许可。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码,受 [MIT 许可协议](https://github.com/uds-se/fuzzingbook/blob/master/LICENSE.md#mit-license) 的许可。 [最后更改:2023-11-11 18:18:06+01:00](https://github.com/uds-se/fuzzingbook/commits/master/notebooks/RailroadDiagrams.ipynb) • 引用 • [版权信息](https://cispa.de/en/impressum)

## 如何引用此作品

Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler: "[Railroad Diagrams](https://www.fuzzingbook.org/html/RailroadDiagrams.html)". In Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler, "[The Fuzzing Book](https://www.fuzzingbook.org/)", [`www.fuzzingbook.org/html/RailroadDiagrams.html`](https://www.fuzzingbook.org/html/RailroadDiagrams.html). Retrieved 2023-11-11 18:18:06+01:00.

```py
@incollection{fuzzingbook2023:RailroadDiagrams,
    author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
    booktitle = {The Fuzzing Book},
    title = {Railroad Diagrams},
    year = {2023},
    publisher = {CISPA Helmholtz Center for Information Security},
    howpublished = {\url{https://www.fuzzingbook.org/html/RailroadDiagrams.html}},
    note = {Retrieved 2023-11-11 18:18:06+01:00},
    url = {https://www.fuzzingbook.org/html/RailroadDiagrams.html},
    urldate = {2023-11-11 18:18:06+01:00}
}

posted @ 2025-12-13 18:14  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报