跳转到主要内容

一个简单、Pythonic的文件格式。与

项目描述

perky

一个友好、简单、Pythonic的文本文件格式

版权所有 2018-2024 Larry Hastings

# test badge # coverage badge # python versions badge

概述

Perky是一种新的简单Pythonic "rcfile"文本文件格式,用于Python程序。它解决了与"INI"文件、"TOML"文件和"JSON"文件相同的问题,但对其最佳解决方案有自己的见解。

Perky的特点

  • 最小化、人性化的语法。Perky文件易于手动编写。
  • 显式最小数据类型支持。Perky让您自己处理最终转换,而不是猜测数据类型。
  • 轻量级、简单且快速。Perky的实现小巧且直接。忽略注释和测试代码后,大约有1k行Python代码。更少的行意味着更少的错误!(希望如此!)
  • 灵活且可扩展。Perky通过"pragma"机制允许通过Perky文件扩展其语义。
  • 完全用Python编写,但在现代桌面上每秒仍可解析>300k行。
  • Perky支持Python 3.6+,并且通过了其单元测试套件,覆盖率100%(排除已弃用的部分)。

Perky语法

Perky配置文件看起来像JSON但没有引号。它只支持一组令人惊讶的小值类型

  • 字符串,包括引号字符串和"三重引号字符串"(多行字符串),
  • "映射"(字典),和
  • "序列"(列表)。

Perky是面向行的;单个值在一行上。容器对象使用每行一个内部值。

您可以在内存允许的深度嵌套列表和字典。

与Python本身不同,忽略前导空白。您可以随意使用前导空白,但它不是必需的。(前导空白在三重引号字符串中保留,但具有允许实际值缩进的巧妙语法。)

空白行和注释行(以#开头的行)将被忽略,除非在三个引号字符串内。

Perky还支持“编译指令”,这些指令是行首带有等号的行,可以执行特殊的运行时行为。默认情况下,Perky没有定义任何编译指令--它是供您使用的扩展机制。

以下是一个示例配置文件,展示了Perky能做什么

example name = value
example dict = {
    name = 3
    another name = 5.0
    }
example list = [
    a
    b
    c
    ]
nested dict = {
    name = value
    nesting level 2 = {
        nesting level 3 = {
            and = so on!
            }
        }
    list inside the dict = [
        value in the list
            [
            and this is in a nested list!
            this is another value.
            you see?
            ]
        ]
    }
# lines starting with hash are comments and are ignored!

# blank lines are ignored too!

" quoted name " = " quoted value "

triple quoted string = """

    indenting
        is preserved

    the string is automatically outdented
    to the leftmost character of the *ending*
    triple-quote

    <-- aka here
    """

one-line empty list = []
one-line empty dict = {}
one-line empty list with whitespace = [ ]
one-line empty dict with whitespace = { }
multi-line empty list = [
    ]
multi-line empty dict = {
    }

=pragma
=pragma with argument

显式转换优于隐式转换

Perky的一个可能令人惊讶的设计选择:Perky解析器原生支持的唯一值是字典、列表和字符串。那么整数?浮点数?日期?布尔值?NULLNone呢?

Perky故意将这个问题留给你。正如Python的禅所说

面对歧义,拒绝猜测的诱惑。

Perky不知道你的程序需要什么类型。所以,而不是猜测并出错,Perky使事情简单:只列表、字典和字符串。对于任何其他类型,你必须自己将其从字符串转换为你想要的类型,然后再转换回来。

注意,Perky不关心你如何或是否转换你的数据。你可以直接使用字符串,或者按照你的喜好转换它们。你可以手动转换它们,或者使用第三方数据转换库,如Marshmallow。

(Perky曾经支持一个用于自动数据转换的实验性API。但这从未完全实现,现在有更好的技术版本。我已经弃用了“转换”子模块,并将在此之前将其删除。)

编译指令

编译指令是Perky解析器的元数据指令。它是从Perky文本内部向Perky解析器发送指令的一种方式。在Perky中,编译指令是行首带有未引用等号的行。

以下是一个编译指令示例

=command argument here

等号后的第一个单词是编译指令的名称,在本例中为"command"。编译指令名称之后的所有内容是参数,所有首尾空白都被删除,在本例中为"argument here"

默认情况下,Perky没有任何编译指令处理器。当Perky没有相应的处理器时调用编译指令会导致运行时错误。但你在调用load()loads()时可以定义自己的编译指令处理器,使用名为pragmas的命名参数。如果你为pragmas传递一个值,它必须是一个字符串到函数的映射。字符串名称应该是编译指令的名称,并且必须是小写。它映射到的函数将“处理”该编译指令,应与以下原型匹配

def pragma_fn(parser, argument)

parser是内部Perky Parser对象。 argument是去除首尾空白后的相关行的其余部分。(如果该行其余部分为空,则argumentNone)。编译指令函数的返回值将被忽略。

目前只有一个预定义的编译指令处理器,一个名为pragma_include()的函数。这为Perky添加了“包含语句”功能。如果你调用此

load(filename, pragmas={'include': pragma_include()})

那么Perky将解释filename中开始的以=include开头的行作为包含语句,使用行的其余部分作为文件的名称。更多信息请参见下面的pragma_include()

编译指令规则

  • 要调用编译指令,请使用=作为行首的第一个非空白字符。
  • 编译指令的名称必须始终为小写。
  • 你可以在序列或映射上下文中调用编译指令。但你不能在三个引号字符串内部调用编译指令。
  • 编译指令可以是“上下文感知的”:它们可以知道它们在文件中的运行位置,例如修改当前字典或列表。编译指令函数可以看到正在解析的整个当前嵌套字典和列表列表(通过parser.stack)。
  • pragma名称之后的整行是pragma的参数值,如果有。这始终是一个字符串,可以是未引用的或单引号引用的;如果未引用,则不能包含任何特殊符号({ } = ''' """)。
  • 如果您想使一行以等号开头(一个value或一个name=value),但又不想它是一个pragma,只需在它周围加上引号。同样,如果您想在pragma参数中使用特殊符号,只需在它周围加上(单引号)。

解析器对象

Pragma函数接收Perky Parser对象作为参数。该对象封装了当前时间解析Perky文件的所有当前状态。以下是您可能希望从pragma中使用的相关属性

  • parser.source包含当前Perky文本的源,要么是文件名,要么是字符串''。
  • parser.line_number包含当前正在解析的行的行号。Perky文本的第一行是第1行。
  • parser.stack是一个引用集合对象的栈--从Perky文件顶部开始的嵌套字典和列表的栈,反映我们现在的位置。parser.stack[0]始终是根,将由loadloads返回的对象。 parser.stack[-1]始终是pragma运行时的当前上下文。它可以是列表或字典。您应该使用isinstance(parser.stack[-1], collections.abc.Mapping)来确定这一点;如果是True,当前上下文是映射上下文(一个字典),如果是False,当前上下文是序列上下文(一个列表)。

解析错误

在解析Perky文本时,只有几种可能的错误

  • 显然,语法错误,例如
    • 字典中一行没有未引用的等号
    • 列表中看起来像字典行(name = value)。(如果您想在列表中包含包含等号的值,只需将其放在引号中。)
    • 三引号字符串中任何行的缩进超出了结束三引号行。
  • 在同一个字典中定义相同的值两次。这被标记为错误,因为它可能是一个错误,并且像Python一样,我们不想让错误无声通过。
  • 使用未定义的pragma。
  • 使用Perky的特殊标记作为pragma参数,如{['''"""[]{}

API

loads(s, *, pragmas=None, root=None)

解析Perky格式的字符串,并返回一个包含从该字符串解析出的值的容器。

如果pragmas不是None,它必须是一个字符串到pragma处理函数的映射。请参阅文档中的Pragmas部分。

如果rootNoneloads的行为就像您传递了一个空的dict

如果root不是None,它应该是一个容器对象,要么是可变的映射(类型dict),要么是可变的序列(类型list)。这会影响数据的解析;如果root是可变的映射对象,Perky文件的最高级别必须是“映射上下文”(一系列name=value行);如果root是可变的序列对象,Perky文件的最高级别假定是“序列上下文”(一系列value行)。

load(filename, *, pragmas=None, root=None)

加载包含Perky文件格式设置的文件。返回一个字典。

文件中的文本必须使用UTF-8编码。

如果rootNoneloads的行为就像您传递了一个空的dict

如果root不是None,它应该是一个容器,要么是可变的映射(dict),要么是可变的序列(list)。这会影响数据的解析;如果root是可变的映射,Perky文件的最高级别必须是“映射上下文”(一系列name=value行);如果root是可变的序列,Perky文件的最高级别假定是“序列上下文”(一系列value行)。

如果pragmas不是None,它必须是一个字符串到pragma处理函数的映射。请参阅文档中的Pragmas部分。

dumps(d)

将字典转换为Perky文件格式的字符串。字典中的键必须是字符串。非字典、列表或字符串的值将使用str转换为字符串。返回一个字符串。

dump(filename, d)

使用dump将字典转换为Perky文件格式的字符串,然后将其写入filename

文件中的文本将使用UTF-8编码。

pragma_include(include_path=(".",), jail=False)

此函数创建并返回一个实现Perky "include"功能的pragma处理器。包含文件意味着将一个Perky文件在另一个文件中按词法插入,在调用pragma的位置上下文中。

例如,如果您运行以下代码

d = loads(
    """
    a=3
    =include data.pky
    c=5
    """,
    pragmas={"include": pragma_include()},
    )

并且当前目录中的data.pky可读,并包含以下文本

b=4

d将被设置为以下字典

{'a': '3', 'b': '4', 'c': '5'}

pragma_include不是pragma处理器本身;它返回一个函数(闭包),该函数记住其配置。

include_path参数允许您指定要搜索包含文件的有序目录列表。它必须是一个可迭代的(要么是tuplelist),包含strpathlib.Path对象。默认情况下,它只包含一个条目'.',这意味着它只会包含当前目录路径下的文件。

jail参数允许激活安全预防措施。默认情况下,jail为false,这意味着pragma_include将愉快地读取您磁盘上的任何文件。

   =include ../../../../../secretfile
   =include ~/.history
   =include /etc/passwd

如果jail为true,则pragma_include只允许读取include_path中指定的目录(或目录)内的文件。如果pragma调用尝试读取这些目录之外的文件,则pragma_include将引发PermissionError

您可以在同一个loadloads调用中使用多个pragma_include处理器,以允许从不同的路径包含,例如。

include_dirs = [appdirs.user_data_dir(myapp_name)]
config_dirs = [appdirs.user_config_dir(myapp_name)]
pragmas = {
    'include': pragma_include(include_dirs),
    'config': pragma_include(config_dirs),
}
d = load(path, pragmas=pragmas)

注意

  • 此pragma处理器是上下文相关的;包含的文件将表现得像被复制粘贴,替换pragma行。在许多方面,这意味着如果pragma在序列上下文中调用,则包含的文件必须开始在序列上下文中。

  • 在加载包含的文件时,pragma处理器将当前pragma处理器传递给load()。在许多方面,这允许递归包含。

  • 当在字典上下文中包含时,您明确允许重新定义在另一个文件中先前定义的现有键。这是双向的;内部(包含)文件可以覆盖外部文件(包含它的文件)中定义的键,外部文件也可以覆盖包含文件中的键。然而,Perky仍然强制执行每个上下文中只能定义一次键的规则。

  • include_path的默认值仅搜索当前目录(".")。如果您覆盖默认值并传递自己的包含路径,则pragma处理器不会搜索当前目录,除非您自己显式地将"."添加到包含路径中。

  • 如果pragma_include在其搜索路径中找不到请求的文件,它将引发FileNotFoundError

已弃用的API

Perky有一个“转换”子模块。想法是,您加载一个Perky文件,然后在那个字典上运行transform以将字符串转换为原生值。

这些函数不再维护或支持,已从coverage测试中排除,并将在新版本1.0之前移除。为什么?Perky的这个部分始终是一个实验……而这个实验从未真正成功。有更好的实现这个想法的方法,比如Marshmallow——你应该使用那些。如果你依赖于Perky中的这段代码,我鼓励你复制一份并自行维护。但我不认为有人会这么做。

为了未来,以下是现在已弃用的API的文档。

map(d, fn) -> o

遍历字典。返回一个新的字典,其中对于每个

  • 如果是字典,则替换为新的字典。
  • 如果是列表,则替换为新的列表。
  • 如果不是字典或列表,则替换为fn(value)

传入的函数称为转换函数

transform(d, schema, default=None) -> o

递归地将Perky字典转换为另一个对象(通常是字典),使用提供的模式。返回一个新的字典。

模式是一个与d的一般预期形状相匹配的数据结构,其中值是字典、列表和可调用对象。转换类似于map(),但每个值将具有单独的转换函数。此外,可以为d中的任何值指定模式转换函数,甚至是字典或列表。

default是一个默认转换函数。如果d中存在一个值v,它没有在schema中找到等效条目,并且v既不是列表也不是字典,并且default是一个可调用对象,则将v替换为default(v)在输出中。

必需

实验性的。

nullable(fn) -> fn

实验性的。

const(fn) -> o

实验性的。

待办事项

  • 反斜杠引号目前做“你Python版本所做的任何事情”。这可能应该是明确的,并由Perky本身解析?

变更日志

0.9.3 2024/09/18

两个新功能,都是针对pragma_include

  • pragma_include现在接受一个新的jail参数,一个布尔值。默认情况下,jail为false。如果jail为true,则包含文件的路径必须在include_paths的路径中或其下。
  • pragma_include现在允许在include_paths参数中使用pathlib.Path对象。当然,它仍然支持str对象。

此外,还有一些常规的文档改进,并将版权通知更新至2024年。

0.9.2 2023/07/22

这是一个极其小的版本。没有新功能或错误修复。

  • 添加了GitHub Actions集成。在每次提交后,在云端运行测试和覆盖率。感谢Dan Pope耐心地引导我完成这个过程!
  • 修复了pyproject.toml文件中的元数据。
  • 放弃了Python 3.5的支持。(我假设我已经放弃了,但它仍然列在项目元数据中作为支持版本。)
  • 添加了测试、覆盖率和支持的Python版本徽章。

0.9.1 2023/07/03

  • API更改:将Parser属性breadcrumbs重命名为stack。它以前未记录,尽管从这个版本开始,它现在是记录的并且是官方支持的。之前的(未记录,未支持)名称breadcrumbs目前保留为别名,但将在1.0之前删除。
  • Parser对象添加了line_numbersource属性,以便于pragma处理器。
  • parser_include进行了轻微的重构。没有更改功能或行为,只是进行了小的代码清理。
  • 在基准程序中添加了“每秒行数”输出度量。

0.9 2023/07/02

破坏性API更改:完全移除了encoding参数。

  • 从现在开始,Perky 仅支持以 UTF-8 编码读取和写入文件。如果您需要使用不同的编码,您必须自行处理加载和保存到磁盘的操作。您需要使用 loadsdumps 来处理 Perky 字符串格式与原生 Python 对象之间的转换。

  • 进一步优化了 Perky。与 0.8.1 版本相比,大约快了 11%。

0.8.2 2023/06/30

  • API 略有改动

    • 现在您可以将 encoding 关键字参数传递给 pragma_include。这是指定通过 pragma_include 加载的文件的编码的唯一方式。
    • 移除了 Perky 的 Parser 对象的(未记录的)encoding 属性。
    • 移除了 loadsencoding 参数。
    • loadencoding 参数现在仅由 load 本身用于加载顶层 Perky 文件。

0.8.1 2023/06/26

  • 哎呀!一个严重的回归:我不小心将非字符串值的默认转换从 str 改为了 repr。这是一个糟糕的改动!str 要好得多。添加了一个测试,以免再次发生这种情况。

0.8 2023/06/25

  • Perky 现在明确使用 collections.abc.MutableMappingcollections.abc.MutableSequence 进行 isinstance 检查,而不是 dictlist。这允许您使用自己的映射和序列对象,这些对象 继承自 dictlist

  • PerkyFormatError 重命名为 FormatError。旧名称目前仍然支持,但请过渡到新名称。旧名称将在 1.0 版本之前被移除。

  • "transformation" 子模块现在已弃用且不再受支持。请停止使用它,或者分叉并自行维护它。这包括 maptransformRequirednullableconst

  • Perky 现在有了一个适当的单元测试套件,它以 100% 的覆盖率通过测试——除了不受支持的 transform 子模块。

  • 在追求 100% 覆盖率的同时,也对代码进行了一些清理。

    • 重制了 LineTokenizer

      • 将其名称从 LineParser 改为 LineTokenizer。它从不进行解析,只是进行标记化。
      • 使 API 更加一致:现在,唯一会抛出 StopIteration 的函数是 __next__
      • 之前可能抛出 StopIteration 的其他函数现在在迭代器为空时返回一个包含 None 值的元组。这意味着您可以安全地编写 for a, b, c in line_tokenizer:
      • bool(lt) 现在是准确的;如果它返回 True,您可以调用 next(lt)lt.next_line()lt.tokens() 并确信您会得到一个值。
    • RuntimeError 异常替换为更合适的异常(ValueErrorTypeError)。

项目详情


下载文件

下载适用于您平台的应用程序。如果您不确定要选择哪一个,请了解有关 安装包 的更多信息。

源代码分发

perky-0.9.3.tar.gz (35.5 kB 查看哈希值)

上传于 源文件

构建的发行版

perky-0.9.3-py3-none-any.whl (22.1 kB 查看哈希值)

上传于 Python 3

支持