跳转至

工作区元数据(Workspace Metadata)

uv workspace metadata 将 uv 掌握的关于工作区或 PEP 723 脚本的信息导出为 JSON,以便其他工具使用。特别是,如果你需要访问 uv.lock 或脚本锁文件中的信息,应优先使用此命令的输出,因为锁文件并非我们保证其稳定性的格式。通过 --script path/to/script.py 参数可以请求脚本的元数据。

主要结构是 "resolution" 字段,其中包含 uv.lock 所编码的依赖图(dependency graph),包含精确的包版本。

图的边(edge)是每个节点定义的 dependencies。这些是安装该节点时也必须安装的依赖(以及它们的 dependencies 递归地传递下去,需注意该图中出现循环是完全正常的)。每个依赖条目将包含一个 id 用于引用所指向的节点,以及一个可选的 marker,用于指定该依赖在哪些平台上需要(如果没有标记,则该依赖始终需要)。

图中源自包的节点由包 name(名称)、version(版本)、source(来源)和 kind(类型)唯一标识。脚本和工作区节点由其路径标识。工作区根依赖组节点由其组名和工作区路径标识。所有节点 id 应被视为不透明(opaque)的。

图中有 5 种节点类型:

  • "script" -- PEP 723 脚本及其直接依赖
  • "workspace" -- 工作区根及其工作区专属依赖组
  • "package" -- 包本身
  • { "extra": "extraname" } -- 包定义的额外依赖(extra)
  • { "group": "groupname" } -- 包或工作区根定义的依赖组

(未来我们将为构建环境的依赖添加 "build" 节点。)

如果你想安装 mypackage,找到其 "kind": "package" 节点。该节点还将包含其 sdist、wheel、额外依赖(optional_dependencies)和依赖组(dependency_groups)的信息。

如果你想安装 mypackage[myextra],则找到 mypackage 对应 "kind": { "extra": "myextra" } 的节点(该节点将始终依赖于 mypackage)。如果你想安装 mypackage[extra1, extra2],则找到 mypackage[extra1]mypackage[extra2] 对应的两个节点。

如果你想安装依赖组 mypackage:mygroup,则找到 mypackage 对应 "kind": { "group": "mygroup" } 的节点(该节点 不会 依赖于 mypackage,因为依赖组只是你在处理包本身时可能需要的依赖列表)。

如果工作区根定义了依赖组但其本身不是一个包,则其 "workspace" 节点通过 dependency_groups 提供相应的组节点 id。

处理同一包的多个版本

一个包的两个版本不能安装到同一个 Python 环境中,但依赖图可能仍包含同一包的多个版本。这可能由两种不同原因导致。

第一种方式是不同平台有冲突的需求,迫使使用不同的包版本。

第二种方式是当工作区存在冲突时,意味着某些工作区成员或其额外依赖是互斥的,同一时间只能安装其中一个。关于冲突的信息可以在顶层的 conflicts 字段中找到。

我们提供的具体保证是:对于任何具体的标记选择,如果你选择了一组没有冲突的包进行安装,那么最终要安装的包集合中将不会包含同一包的多个版本

如果你只想获取"此工作区使用的所有 pydantic 版本",可以遍历节点列表并收集每个实例。然而,如果你想专门分析图并获取实际的解析结果,则可能需要查阅 conflicts 并了解如何为特定平台解析 markers

在处理同一包的多个版本时,避免错误的最佳方法是将对依赖图的查询根植于工作区根、工作区成员或请求的脚本的操作上。这些是图的自然入口点,可以为诸如"安装工作区 dev 组"、"安装 member1member2[extra]"或"安装此脚本声明的依赖"等操作提供一致的响应。

另一种表述方式是:尽可能避免通过遍历 resolution 对象来查找节点。只使用元数据其他部分提供的 id 来像访问映射(map)一样访问 resolution。对于工作区,工作区根是 workspace.id,包入口点列在 members 数组中。对于脚本,初始 id 是 script.id。从那里你可以通过跟随依赖边递归地发现其他包。

因此,与其直接在依赖图中查找 anyio 的节点,不如先决定要分析哪些工作区成员,就好像它们即将被安装一样。在遍历你要安装的依赖项的 dependencies 时,你可能会访问到 anyio 的一个实例,这就是你应该使用的那个。如果你访问到多个 anyio 实例,则意味着你选择了一组有冲突的待安装项,而 uv 永远不会选择这样的组合。

因此,如果你想分析安装工作区成员 mypackagedev 依赖组,大致如下:

member = find_by_name(metadata.members, "mypackage")
member_node = metadata.resolution[member.id]
group = find_by_name(member_node.dependency_groups, "dev")
group_node = metadata.resolution[group.id]
visit(metadata, [group_node])

对于工作区根上定义的依赖组,通过工作区节点查找:

workspace_node = metadata.resolution[metadata.workspace.id]
group = find_by_name(workspace_node.dependency_groups, "dev")
group_node = metadata.resolution[group.id]
visit(metadata, [group_node])

如果你想分析两个特定工作区成员一起安装的情况,大致如下:

to_analyze = []
for member_name in ["package1", "package2"]:
  member = find_by_name(metadata.members, member_name)
  member_node = metadata.resolution[member.id]
  to_analyze.append(member_node)
visit(metadata, to_analyze)

对于脚本,以完全相同的方式从其解析节点开始:

script_node = metadata.resolution[metadata.script.id]
visit(metadata, [script_node])

其中 visit 是你喜欢的图遍历算法,例如深度优先搜索(depth-first search):

def visit(metadata: UvMetadata, to_analyze: list[Node]):
  visited = set()
  while len(to_analyze) > 0:
    node = to_analyze.pop()

    # 通过避免重复访问节点来处理循环
    if node.id in visited:
      continue
    visited.add(node.id)

    # 我们还需要分析其依赖项
    for dependency in node.dependencies:
      # 仅当边满足目标平台的标记时才跟随
      if dependency.marker and not satisfies(platform, dependency.marker):
        continue
      to_analyze.append(metadata.resolution[dependency.id])

    # 分析我们遇到的每个包节点
    if node.kind == "package":
      print(node.name, node.version, node.source)

模式(Schema)

格式的完整 JSON Schema 将在格式定稿后提供。

以下是一个带注释的、人类可读的示例:

{
  // 关于此输出模式的信息
  "schema": {
    // 此输出的版本,当前为 "preview"
    "version": "preview"
  },
  // 可以找到 uv.lock 的目录
  "workspace_root": "/workspace",
  // 关于环境的信息,目前仅在使用 `--sync` 时可用
  "environment": {
    // 环境根目录的绝对路径
    "root": "/workspace/.venv"
  },
  // 关于脚本目标的信息,仅在使用 `--script` 时存在。
  // 工作区元数据改用下面的 `workspace` 和 `members` 作为图入口点。
  "script": {
    // 脚本的绝对路径
    "path": "/workspace/script.py",
    // 脚本节点在下面 `resolution` 映射中的 id
    "id": "script+/workspace/script.py"
  },
  // 关于工作区目标的信息,使用 `--script` 时省略。
  "workspace": {
    // 工作区根的绝对路径
    "path": "/workspace",
    // 工作区节点在下面 `resolution` 映射中的 id
    "id": "workspace+/workspace"
  },
  // 此工作区对 Python 版本的任何要求
  //
  // `marker` 字段都有此隐式约束,为简洁起见而省略
  "requires_python": ">=3.12",
  // 工作区成员列表
  "members": [
    {
      // 包的名称
      "name": "mypackage",
      // 包含其 pyproject.toml 的目录
      "path": "/workspace/packages/mypackage",
      // 此包信息在下面 `resolution` 映射中的 id
      "id": "mypackage==0.1.0@editable+/workspace/packages/mypackage"
    },
  ],
  // 一组互斥的工作区项集合,不能同时安装,
  // 大概是因为它们需要安装同一包的不同版本。
  //
  // 任何尝试安装属于同一集合的两个项的尝试都必须被拒绝。
  //
  // 共有 3 种项:
  //
  // * Project -- "kind": "project"
  // * Extra   -- "kind": { "extra": "extraname" }
  // * Group   -- "kind": { "group": "groupname" }
  "conflicts": {
    "sets": [
      {
        "items": [
          {
            "package": "mypackage",
            "kind": { "extra": "myextra" }
            "id": "mypackage[myextra]==0.1.0@editable+/workspace/packages/mypackage",
          }
          {
            "package": "mypackage",
            "kind": { "group": "mygroup" }
            "id": "mypackage:mygroup==0.1.0@editable+/workspace/packages/mypackage",
          }
        ]
      }
    ]
  }
  // 关于包和依赖的已解析信息。
  //
  // 此映射中的每个条目都是依赖图中的一个节点。目前
  // 依赖图中有 5 种节点,不过未来计划添加更多。
  //
  // * Scripts  -- "kind": "script"
  // * Workspaces -- "kind": "workspace"
  // * Packages -- "kind": "package"
  // * Extras   -- "kind": { "extra": "extraname" }
  // * Groups   -- "kind": { "group": "groupname" }
  //
  // 包节点包含大部分元数据,而其他节点主要只是一个依赖列表。
  // 包含不同类型的节点是为了鼓励对图的正确分析。例如,
  // `mypackage[someextra]` 的节点始终依赖于 `mypackage`,
  // 而 `mypackage:somegroup` 则不会(因为依赖组只是
  // 在处理 `mypackage` 时可能想安装的包列表)。像
  // `mypackage[extra1, extra2]` 这样的语法糖会被分解为
  // 对 `mypackage[extra1]` 和 `mypackage[extra2]` 的单独依赖。
  //
  // 此处使用的 id 是人类可读的,但应作为不透明值处理(节点中
  // 包含的相同信息以更方便的形式提供)。
  "resolution": {

    // 当使用 `--script` 请求元数据时,脚本节点存在。其依赖项
    // 是脚本声明的直接需求。
    "script+/workspace/script.py": {
      "kind": "script",
      "path": "/workspace/script.py",
      "dependencies": [
        {
          "id": "iniconfig==2.0.0@registry+https://pypi.org/simple"
        }
      ]
    },

    // 工作区节点拥有直接定义在工作区根上的元数据。
    "workspace+/workspace": {
      "kind": "workspace",
      "path": "/workspace",
      "dependencies": [],
      "dependency_groups": [
        {
          "name": "dev",
          "id": "workspace+/workspace:dev"
        }
      ]
    },

    // 此节点是定义在非包工作区根上的依赖组。
    "workspace+/workspace:dev": {
      "kind": { "group": "dev" },
      "path": "/workspace",
      "dependencies": [
        {
          "id": "iniconfig==2.0.0@registry+https://pypi.org/simple"
        }
      ]
    },

    // 此节点是一个工作区成员
    "mypackage==0.1.0@editable+/workspace/packages/mypackage": {
      // 包的名称
      "name": "mypackage",
      // 包的版本(可能缺失,因为源代码树不需要版本)
      "version": "0.1.0",
      // 包的来源,此例中是一个 editable,其相对于
      // `workspace_root` 的路径为 `./packages/mypackage`
      "source": {
        "editable": "/workspace/packages/mypackage"
      },
      // 节点的类型,此例中为 "package"(有关详细信息,请参阅上面关于 `resolution` 的文档)
      "kind": "package",
      // 要将此节点安装到环境中也必须安装的依赖项
      "dependencies": [
        {
          // 要查找详细信息的节点 id
          "id": "iniconfig==2.0.0@registry+https://pypi.org/simple"
          "marker": "marker": "sys_platform == 'linux'"
        }
      ],
      // 此包定义的额外依赖
      "optional_dependencies": [
        {
          "name": "myextra",
          "id": "mypackage[myextra]==0.1.0@editable+/workspace/packages/mypackage"
        }
      ],
      // 此包定义的依赖组
      "dependency_groups": [
        {
          "name": "mygroup",
          "id": "mypackage:mygroup==0.1.0@editable+/workspace/packages/mypackage"
        }
      ]
    },

    // 此节点是工作区成员的一个 extra
    "mypackage[myextra]==0.1.0@editable+/workspace/packages/mypackage": {
      // 这些字段将与上面的包节点匹配
      "name": "mypackage",
      "version": "0.1.0",
      "source": {
        "editable": "/workspace/packages/mypackage"
      },
      // 但这两个字段将与上面的包节点不同
      "kind": { "extra": "myextra" },
      "dependencies": [
        {
          "id": "mypackage==0.1.0@editable+/workspace/packages/mypackage"
        }
        {
          "id": "anyio==2.0.0@registry+https://pypi.org/simple"
        }
      ]
    },

    // 此节点是工作区成员的一个依赖组
    "mypackage:mygroup==0.1.0@editable+/workspace/packages/mypackage": {
      // 这些字段将与上面的包节点匹配
      "name": "mypackage",
      "version": "0.1.0",
      "source": {
        "editable": "/workspace/packages/mypackage"
      },
      // 但这两个字段将与上面的包节点不同
      "kind": { "extra": "myextra" },
      "dependencies": [
        {
          "id": "anyio==1.0.0@registry+https://pypi.org/simple"
        }
      ]
    },

    // 此节点是 PyPI 上的一个包
    "iniconfig==2.0.0@registry+https://pypi.org/simple": {
      "name": "iniconfig",
      "version": "2.0.0",
      // registry 来源如下所示
      "source": {
        "registry": {
          "url": "https://pypi.org/simple"
        }
      },
      "kind": "package",
      "dependencies": [],
      // 包的源代码分发包(source distribution)的详细信息
      "sdist": {
        // 也可能是 `path`
        "url": "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz",
        "hashes": {
          "sha256": "2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"
        },
        "size": 4646,
        "upload_time": "2023-01-07T11:08:11.254Z"
      },
      // 我们为此包找到的 wheel
      "wheels": [
        {
          // 也可能是 `path`
          "url": "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl",
          "hashes": {
            "sha256": "b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
          },
          "size": 5892,
          "upload_time": "2023-01-07T11:08:09.864Z",
          // 解析此文件名可以了解 wheel 支持的平台
          "filename": "iniconfig-2.0.0-py3-none-any.whl"
        }
      ]
    }

    // ...以此类推
    "anyio==1.0.0@registry+https://pypi.org/simple": { ... }
    "anyio==2.0.0@registry+https://pypi.org/simple": { ... }
  }
}