Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python增强提案

PEP 750 – 用于编写领域特定语言的标签字符串

作者:
Jim Baker <jim.baker at python.org>,Guido van Rossum <guido at python.org>,Paul Everitt <pauleveritt at me.com>
赞助人:
Lysandros Nikolaou <lisandrosnik at gmail.com>
讨论列表:
Discourse 线程
状态:
草稿
类型:
标准跟踪
创建:
2024年7月8日
Python版本:
3.14

目录

摘要

本 PEP 引入了用于自定义、可重复字符串处理的标签字符串。标签字符串是对 f-字符串的扩展,使用自定义函数(“标签”)代替 f 前缀。然后,此函数可以提供丰富的功能,例如安全检查、惰性求值、用于 Web 模板的领域特定语言 (DSL) 等。

标签字符串类似于 JavaScript 标记模板字面量 和其他语言中的相关概念。以下标签字符串用法显示了它与 f-字符串的相似之处,尽管它能够处理字面字符串和嵌入的值

name = "World"
greeting = greet"hello {name}"
assert greeting == "Hello WORLD!"

标签函数接受准备好的参数并返回一个字符串

def greet(*args):
    """Tag function to return a greeting with an upper-case recipient."""
    salutation, recipient, *_ = args
    getvalue, *_ = recipient
    return f"{salutation.title().strip()} {getvalue().upper()}!"

您可以在下面找到更丰富的示例。需要注意的是,本文档中讨论了基于 CPython 3.14 的实现。

与其他PEP的关系

Python 在 Python 3.6 中通过 PEP 498 引入了 f-字符串。然后,语法在 PEP 701 中被形式化,该 PEP 还取消了一些限制。本 PEP 基于 PEP 701。

在 PEP 498 出现的同时,PEP 501 被编写用于提供“i-字符串”——即“插值模板字符串”。该 PEP 被推迟,等待 f-字符串的更多经验。2023 年 3 月,另一位作者恢复了对该 PEP 的工作,引入了“t-字符串”作为模板字面量,并构建在 PEP 701 之上。

本 PEP 的作者认为标签字符串是对 PEP 501 中更新工作的概括。

动机

Python f-字符串非常快地变得非常流行。语法简单、方便,插值表达式可以访问常规的作用域规则。但是,f-字符串有两个主要限制——表达式被急切地求值,并且无法拦截插值值。前者意味着 f-字符串不能像模板一样重复使用,后者意味着无法自定义值的插值方式。

Python 中的模板化目前是通过 Jinja2 等包实现的,这些包带来了自己的模板语言来生成动态内容。除了需要学习更多内容之外,这些语言的表达能力远不如 Python 本身。这意味着无法用模板语言表达的业务逻辑必须用 Python 编写,从而导致逻辑分散在不同的语言和文件中。

同样,无法拦截插值值意味着在将它们集成到最终字符串之前无法对其进行清理或转换。在这里,f-字符串的便利性可能被认为是一种负担。例如,使用 sqlite3 执行查询的用户可能会倾向于使用 f-字符串将值嵌入到他们的 SQL 表达式中,而不是使用 ? 占位符并将值作为元组传递以避免 SQL 注入攻击

标签字符串通过扩展 f-字符串语法来解决这两个问题,以便开发人员在组合字符串及其插值值之前能够访问它们。这样做,标签字符串可以以多种不同的方式解释,从而为 DSL 和其他自定义字符串处理打开了可能性。

提案

本 PEP 提出为 f-字符串提供可自定义的前缀。然后,这些 f-字符串成为“标签字符串”:带有“标签函数”的 f-字符串。标签函数是一个可调用对象,它接收字符串中已解析标记的参数序列。

这是一个非常简单的示例。假设我们想要某种具有某些自定义业务策略的字符串:将值大写并添加感叹号。

让我们从一个简单返回静态问候语的标签字符串开始

def greet(*args):
    """Give a static greeting."""
    return "Hello!"

assert greet"Hi" == "Hello!"  # Use the custom "tag" on the string

如您所见,greet 只是一个可调用对象,位于 f 前缀所在的位置。让我们看看 args

def greet(*args):
    """Uppercase and add exclamation."""
    salutation = args[0].upper()
    return f"{salutation}!"

greeting = greet"Hello"  # Use the custom "tag" on the string
assert greeting == "HELLO!"

标签函数传递一个参数序列。由于我们的标签字符串只是 "Hello",因此 args 序列仅包含一个类似字符串的值 'Hello'

有了这个,让我们引入一个 *插值*。也就是说,应该插入值的地方

def greet(*args):
    """Handle an interpolation."""
    # The first arg is the string-like value "Hello " with a space
    salutation = args[0].strip()
    # The second arg is an "interpolation"
    interpolation = args[1]
    # Interpolations are tuples, the first item is a lambda
    getvalue = interpolation[0]
    # It gets called in the scope where it was defined, so
    # the interpolation returns "World"
    result = getvalue()
    recipient = result.upper()
    return f"{salutation} {recipient}!"

name = "World"
greeting = greet"Hello {name}"
assert greeting == "Hello WORLD!"

{name} 的 f-字符串插值导致了标签字符串中的新机制

  • args[0] 仍然是类似字符串的 'Hello ',这次带有一个尾随空格
  • args[1] 是一个表达式——{name} 部分
  • 标签字符串将这部分表示为一个 *插值* 对象,如下所述

*args 列表是 DecodedInterpolation 值的序列。“解码”对象是一个类似字符串的对象,具有额外的功能,如下所述。“插值”对象是一个类似元组的值,表示 Python 如何将插值处理成对标签函数有用的形式。这两者都将在下面的 规范 中进行完整描述。

这是一个使用结构化模式匹配和类型提示的更通用版本

from typing import Decoded, Interpolation  # Get the new protocols

def greet(*args: Decoded | Interpolation) -> str:
    """Handle arbitrary args using structural pattern matching."""
    result = []
    for arg in args:
        match arg:
            case Decoded() as decoded:
                result.append(decoded)
            case Interpolation() as interpolation:
                value = interpolation.getvalue()
                result.append(value.upper())

    return f"{''.join(result)}!"

name = "World"
greeting = greet"Hello {name} nice to meet you"
assert greeting == "Hello WORLD nice to meet you!"

标签字符串从 Interpolation 中提取的不仅仅是可调用对象。它们还提供 Python 字符串格式信息以及原始文本

def greet(*args: Decoded | Interpolation) -> str:
    """Interpolations can have string formatting specs and conversions."""
    result = []
    for arg in args:
        match arg:
            case Decoded() as decoded:
                result.append(decoded)
            case getvalue, raw, conversion, format_spec:  # Unpack
                gv = f"gv: {getvalue()}"
                r = f"r: {raw}"
                c = f"c: {conversion}"
                f = f"f: {format_spec}"
                result.append(", ".join([gv, r, c, f]))

    return f"{''.join(result)}!"

name = "World"
assert greet"Hello {name!r:s}" == "Hello gv: World, r: name, c: r, f: s!"

您可以看到每个 Interpolation 部分都被提取出来

  • 要调用的 lambda 表达式以及它定义的作用域中的值
  • 插值的原始字符串 (name)
  • Python “转换”字段 (r)
  • 任何 格式规范 (s)

规范

在本规范的其余部分,my_tag 将用于任意标签。例如

def mytag(*args):
    return args

trade = 'shrubberies'
mytag'Did you say "{trade}"?'

有效的标签名称

标签名称可以是任何未带点的名称,该名称不是现有有效的字符串或字节前缀,如 词法分析规范 中所示。因此,这些前缀不能用作标签

stringprefix: "r" | "u" | "R" | "U" | "f" | "F"
            : | "fr" | "Fr" | "fR" | "FR" | "rf" | "rF" | "Rf" | "RF"

bytesprefix: "b" | "B" | "br" | "Br" | "bR" | "BR" | "rb" | "rB" | "Rb" | "RB"

Python 限制某些关键字 用作标识符。此限制也适用于标签名称。关键字的使用应触发有用的错误,就像最近的 CPython 版本中所做的那样。

标签必须紧接在引号之前

与其他字符串字面量前缀一样,标签和引号之间不能有空格。

PEP 701

标签字符串支持 PEP 701 的完整语法,因为任何字符串字面量,使用任何引号,都可以嵌套在插值中。当然,此嵌套包括标签字符串。

评估标签字符串

当标签字符串被求值时,标签必须有一个绑定,否则会引发 NameError;并且它必须是一个可调用对象,否则会引发 TypeError。可调用对象必须接受一系列位置参数。此行为遵循以下内容的去糖化:

trade = 'shrubberies'
mytag'Did you say "{trade}"?'

mytag(DecodedConcrete(r'Did you say "'), InterpolationConcrete(lambda: trade, 'trade', None, None), DecodedConcrete(r'"?'))

注意

DecodedConcreteInterpolationConcrete 只是示例实现。如果获得批准,标签字符串将在 builtins 中具有具体类型。

解码后的字符串

mytag'Did you say "{trade}"?' 示例中,有两个字符串:r'Did you say "'r'"?'

字符串在内部存储为具有 Decoded 结构的对象,这意味着:符合协议 Decoded

@runtime_checkable
class Decoded(Protocol):
    def __str__(self) -> str:
        ...

    raw: str

这些 Decoded 对象可以访问原始字符串。使用原始字符串是因为标签字符串旨在针对各种 DSL,例如 shell 和正则表达式。此类 DSL 对元字符(即反斜杠)有其自己的特定处理方式。

但是,通常需要“已处理”字符串,方法是像标准 Python 字符串一样解码字符串。在提议的实现中,解码对象的 __new__ 将 *存储* 原始字符串并 *存储和返回* “已处理”字符串。

该协议被标记为 @runtime_checkable 以允许结构化模式匹配测试协议而不是类型。这可能会产生很小的性能损失。由于 case 测试位于用户代码标签函数中,因此作者可以选择通过测试接下来讨论的实现类型来进行优化。

Decoded 协议将可从 typing 获取。在 CPython 中,Decoded 将在 C 中实现,但对于本 PEP 的讨论,以下是一个兼容的实现

class DecodedConcrete(str):
    _raw: str

    def __new__(cls, raw: str):
        decoded = raw.encode("utf-8").decode("unicode-escape")
        if decoded == raw:
            decoded = raw
        chunk = super().__new__(cls, decoded)
        chunk._raw = raw
        return chunk

    @property
    def raw(self):
        return self._raw

插值

Interpolation 是表示标签字符串内表达式的的数据结构。插值支持延迟求值模型,其中插值表达式被计算、转换、记忆化或以任何方式处理。

此外,插值表达式的原始文本可供标签函数使用。这对于调试或元编程很有用。

Interpolation 是一个将从 typing 中提供的 Protocol。其定义如下:

@runtime_checkable
class Interpolation(Protocol):
    def __len__(self):
        ...

    def __getitem__(self, index: int):
        ...

    def getvalue(self) -> Callable[[], Any]:
        ...

    expr: str
    conv: Literal["a", "r", "s"] | None
    format_spec: str | None

给定此插值示例

mytag'{trade!r:some-formatspec}'

这些属性如下:

  • getvalue 是插值的一个零参数闭包。在本例中,lambda: trade
  • expr 是插值的表达式文本。例如:'trade'
  • conv 是标签函数将使用的可选转换,可以是 rsa 之一,分别对应 repr、str 和 ascii 转换。请注意,与 f 字符串一样,不支持其他转换。例如:'r'
  • format_spec 是可选的format_spec 字符串。如果 format_spec 包含任何表达式,则会在传递给标签函数之前对其进行急切求值。例如:'some-formatspec'

在所有情况下,标签函数都决定如何处理有效的 Interpolation 属性。

在 CPython 参考实现中,使用 C 实现 Interpolation 将使用等效的结构序列对象(参见诸如 os.stat_result 之类的代码)。出于本 PEP 的目的,以下是一个纯 Python 实现的示例

class InterpolationConcrete(NamedTuple):
    getvalue: Callable[[], Any]
    expr: str
    conv: Literal['a', 'r', 's'] | None = None
    format_spec: str | None = None

插值表达式求值

插值的表达式求值与PEP 498 中相同,只是所有表达式始终隐式地用 lambda 包装。

从字符串中提取的表达式在标签字符串出现的上下文中进行求值。这意味着表达式可以完全访问其词法作用域,包括局部和全局变量。可以使用任何有效的 Python 表达式,包括函数和方法调用。

但是,还需要考虑一个额外的细微差别,即函数作用域注释作用域。考虑以下配置标题的稍微做作的示例

class CaptionConfig:
    tag = 'b'
    figure = f'<{tag}>Figure</{tag}>'

现在让我们尝试重写以上示例以使用标签字符串

class CaptionConfig:
    tag = 'b'
    figure = html'<{tag}>Figure</{tag}>'

不幸的是,如果使用通常的 lambda 包装来实现插值(即 lambda: tag),则此重写不起作用。当标签函数对插值进行求值时,将导致 NameError: name 'tag' is not defined。此名称错误的根本原因是 lambda: tag 使用函数作用域,因此无法使用定义 tag 的类定义。

即使使用 f 字符串,对标签字符串如何求值进行反糖化也会导致相同的 NameError;此处的 lambda 包装也使用函数作用域

class CaptionConfig:
    tag = 'b'
    figure = f'<{(lambda: tag)()}>Figure</{(lambda: tag)()}>'

对于标签字符串,出现此类 NameError 将令人惊讶。在使用标签字符串处理类变量的这种特定情况下,它也将是一个粗糙的边缘。毕竟,标签字符串应该支持 f 字符串功能的超集。

解决方案是为标签字符串插值使用注释作用域。虽然名称“注释作用域”暗示它仅与注释有关,但它通过在类定义(例如 tag)中词法解析名称来解决此问题,这与函数作用域不同。

注意

使用注释作用域意味着无法将插值完全反糖化为 Python 代码。相反,就像编写 interpolation_lambda: tag 而不是 lambda: tag 一样,其中假设的 interpolation_lambda 关键字变体使用注释作用域而不是标准函数作用域。

参考实现或多或少就是这样实现此概念的(当然,没有创建新的关键字)。

因此,本 PEP 及其参考实现使用对注释作用域的支持。请注意,此用法与PEP 649PEP 695 的实现是可分离的部分,后者为注释提供了某种类似的延迟执行模型。相反,由标签函数评估任何插值。

有了注释作用域,插值中的 lambda 包装表达式就可以提供与 f 字符串相同的常用词法作用域。因此,无需使用 locals()globals() 或使用 sys._getframe 进行框架内省来评估插值。此外,每个表达式的代码都可用,无需使用 inspect.getsource 或其他方法查找。

格式规范

如果未在标签字符串的对应插值中指定 format_spec,则默认为 None

由于标签函数完全负责处理 DecodedInterpolation 值,因此插值中的格式规范和转换没有所需的解释。例如,这是一个有效的用法

html'<div id={id:int}>{content:HTML|str}</div>'

在本例中,第二个插值的 format_spec 是字符串 'HTML|str';由 html 标签决定在此处对“格式规范”执行什么操作(如果有的话)。

f-string 样式 = 求值

mytag'{expr=}' 的解析结果与 mytag'expr={expr}’ 相同,如问题 Add = to f-strings for easier debugging 中所实现。

标签函数参数

标签函数具有以下签名

def mytag(*args: Decoded | Interpolation) -> Any:
    ...

这对应于以下协议

class TagFunction(Protocol):
    def __call__(self, *args: Decoded | Interpolation) -> Any:
        ...

由于子类化,mytag 的签名当然可以扩展到以下内容,但代价是会损失一些类型特异性

def mytag(*args: str | tuple) -> Any:
    ...

用户可能会这样编写标签字符串:

def tag(*args):
    return args

tag"\N{{GRINNING FACE}}"

标签字符串会将其表示为正好一个 Decoded 参数。在本例中,Decoded.raw 将为 '\\N{GRINNING FACE}'。“已处理”的表示形式(通过编码和解码)将为

'\\N{GRINNING FACE}'.encode('utf-8').decode('unicode-escape')
'😀'

紧跟更多文本的命名 Unicode 字符仍将产生一个 Decoded 参数

def tag(*args):
    return args

assert tag"\N{{GRINNING FACE}}sometext" == (DecodedConcrete("😀sometext"),)

返回值

标签函数可以返回任何类型。通常它们会返回一个字符串,但可以通过返回更丰富的对象来构建更丰富的系统。请参见下面的示例。

函数应用

标签字符串的反糖化如下:

mytag'Hi, {name!s:format_spec}!'

这等效于

mytag(DecodedConcrete(r'Hi, '), InterpolationConcrete(lambda: name, 'name',
's', 'format_spec'), DecodedConcrete(r'!'))

注意

为简单起见,此反糖化和后续的反糖化省略了插值表达式中名称如何解析的一个重要作用域方面,尤其是在定义类时。请参见插值表达式求值

不允许空解码字符串

解码和插值之间的交替很常见,但这取决于标签字符串。解码字符串的值永远不会为空字符串

mytag'{a}{b}{c}'

…这会导致以下反糖化

mytag(InterpolationConcrete(lambda: a, 'a', None, None), InterpolationConcrete(lambda: b, 'b', None, None), InterpolationConcrete(lambda: c, 'c', None, None))

同样

mytag''

…这会导致以下反糖化

mytag()

富返回值类型的 HTML 示例

标签函数可以通过返回更丰富的对象成为更大处理链中的一个强大组成部分。例如,JavaScript 标记模板字面量不受返回字符串的要求约束。例如,让我们看一下 HTML 生成系统,以及一个用法和“子组件”

def Menu(*, logo: str, class_: str) -> HTML:
    return html'<img alt="Site Logo" src={logo} class={class_} />'

icon = 'acme.png'
result = html'<header><{Menu} logo={icon} class="my-menu"/></header>'
img = result.children[0]
assert img.tag == "img"
assert img.attrs == {"src": "acme.png", "class": "my-menu", "alt": "Site Logo"}
# We can also treat the return type as a string of specially-serialized HTML
assert str(result) = '<header>' # etc.

html 标签函数可能具有以下签名

def html(*args: Decoded | Interpolation) -> HTML:
    ...

作为 ProtocolHTML 返回类可能具有以下形状

@runtime_checkable
class HTML(Protocol):
    tag: str
    attrs: dict[str, Any]
    children: Sequence[str | HTML]

总而言之,返回的实例可以作为

  • 字符串,用于序列化到最终输出
  • 可迭代对象,用于处理 WSGI/ASGI 以按其写入顺序输出流式传输和求值的插值
  • 嵌套 Python 数据的 DOM(数据)结构

在每种情况下,结果都可以以安全的方式延迟且递归地组合,因为返回值不需要是字符串。建议的做法是返回值是“被动”对象。

返回丰富对象而不是字符串可能有哪些好处?HTML 模板等领域的 DSL 可以提供后处理工具链,就像Babel 对 JavaScript使用基于 AST 的转换插件 所做的那样。类似地,提供中间件处理的系统可以在具有更多功能的更丰富、标准的对象上运行。标签字符串结果可以作为嵌套 Python 对象进行测试,而不是字符串操作。最后,中间结果可以以有用的方式缓存/持久化。

工具支持

标签字符串中的 Python 语义

Python 模板语言和其他 DSL 具有与 Python 完全不同的语义。不同的作用域规则、不同的调用语义(例如宏)、它们自己的循环语法等等。

这意味着所有工具都需要为每种语言编写特殊支持。即使这样,通常也很难找到所有可能的作用域,例如自动完成值。

但是,f 字符串没有此问题。f 字符串被视为 Python 的一部分。花括号中的表达式按预期行为,并且值应根据常规作用域规则解析。像 mypy 这样的工具可以查看 f 字符串表达式内部,但可能永远不会查看 Jinja2 模板内部。

使用标签字符串编写的 DSL 将继承许多此值。虽然我们不能期望标准工具理解 DSL 中的“域”,但它们仍然可以检查 f 字符串中可表达的任何内容。

向后兼容性

与 f 字符串一样,使用标签字符串将与以前的版本存在语法上的向后不兼容性。

安全隐患

关于插值,使用插值的工作的安全隐患如下:

  1. 作用域查找与 f 字符串相同(词法作用域)。此模型已在实践中证明效果良好。

  2. 标签函数可以确保任何插值以安全的方式完成,包括尊重目标 DSL 中的上下文。

如何教授

标签字符串有多个受众:标签函数的使用者、标签函数的作者以及提供有趣标签函数机制的框架作者。

所有三个群体都可以从一个重要的框架开始。

  • 现有的解决方案(例如模板引擎)可以完成标签字符串的部分功能。
  • 但标签字符串将逻辑更靠近“正常的 Python”。

使用者可以将标签字符串视为从 f-字符串开始。

  • 它们看起来很熟悉。
  • 作用域和语法规则相同。

他们首先需要吸收:与 f-字符串不同,字符串不会立即“就地”求值。其他东西(标签函数)会发生。这是第二件需要教授的事情:标签函数执行特定的操作。因此产生了“领域特定语言”(DSL)的概念。需要额外教授的内容:在标记字符串之前,需要导入标签函数。

标签函数作者从创建 DSL 的角度思考。他们希望以 Python 熟悉的方式提供业务策略。使用标签函数,Python 将执行大部分预处理。这降低了创建 DSL 的门槛。

标签作者可以从简单的用例开始。在作者获得经验后,可以使用标签字符串添加更大的模式:延迟求值、中间表示、注册表等等。

这些要点中的每一个也与装饰器的教学相匹配。在这种情况下,学习者使用某些东西应用于紧随其后的代码。他们不需要了解太多关于装饰器理论的知识就可以利用其实用性。

编写标签函数时常见的模式

结构化模式匹配

使用结构化模式匹配迭代参数是许多标签函数实现的预期最佳实践。

def tag(*args: Decoded | Interpolation) -> Any:
    for arg in args:
        match arg:
            case Decoded() as decoded:
                ... # handle each decoded string
            case Interpolation() as interpolation:
                ... # handle each interpolation

惰性求值

上面示例标签函数中的每个函数都立即调用插值的 getvalue lambda。Python 开发人员经常希望 f-字符串可以被延迟或延迟求值。编写一个包装器将非常简单,例如,在调用 __str__ 时延迟调用 lambda。

记忆化

标签函数作者可以控制处理静态字符串部分和动态插值部分。为了提高性能,他们可以部署记忆处理方法,例如通过生成键。

求值顺序

假设一个标签生成 HTML 中的多个部分。该标签需要每个部分的输入。但是如果最后一个输入参数需要一段时间呢?在所有参数都可用之前,您无法返回第一部分的 HTML。

您希望在输入可用时发出标记。一些模板工具支持这种方法,标签字符串也支持。

参考实现

在发布此 PEP 时,一个完全可用的实现可用

此实现不是最终版本,因为 PEP 讨论可能会提供更改。

被拒绝的想法

启用 convformat_spec 的精确往返

关于精确往返到原始源文本,有两个限制。

首先,format_spec 可以任意嵌套。

mytag'{x:{a{b{c}}}}'

在此 PEP 和相应的参考实现中,format_spec 被急切地求值以设置插值中的 format_spec,从而丢失原始表达式。

虽然在每个用法中保留往返是可行的,但这将需要一个额外的标志 equals 来支持,例如 {x=},以及用于 format_spec 的递归 Interpolation 定义。以下是此类型的纯 Python 等效项,包括保留序列解包(如在 case 语句中使用)

class InterpolationConcrete(NamedTuple):
    getvalue: Callable[[], Any]
    raw: str
    conv: str | None = None
    format_spec: str | None | tuple[Decoded | Interpolation, ...] = None
    equals: bool = False

    def __len__(self):
        return 4

    def __iter__(self):
        return iter((self.getvalue, self.raw, self.conv, self.format_spec))

但是,支持精确往返的额外复杂性似乎没有必要,因此被拒绝。

不允许隐式字符串连接

不支持隐式标签字符串连接,这与其他字符串文字不同。

预期三引号就足够了。如果支持隐式字符串连接,标签评估的结果将需要支持使用 __add____radd__+ 运算符。

由于标签字符串针对嵌入式 DSL,因此这种复杂性会引入其他问题,例如确定合适的分隔符。这似乎不必要地复杂,因此被拒绝。

任意转换值

Python 仅允许 rsa 作为可能的转换类型值。尝试分配不同的值会导致 SyntaxError

理论上,标签函数可以选择处理其他转换类型。但是此 PEP 严格遵循PEP 701。对允许值的任何更改都应在单独的 PEP 中进行。

致谢

感谢 Ryan Morshead 在开发导致标签字符串的想法期间做出的贡献。还要感谢 Koudai Aono 在贡献材料方面进行的基础设施工作。还要特别感谢 Dropbox 的pyxl,因为它几年前就解决了类似的想法。


来源:https://github.com/python/peps/blob/main/peps/pep-0750.rst

上次修改时间:2024-08-11 11:55:15 GMT