Skip to content

Commit a1e336a

Browse files
feat: update schema from voluptuous to msgspec
1 parent f9b5e2d commit a1e336a

File tree

25 files changed

+2773
-2463
lines changed

25 files changed

+2773
-2463
lines changed

docs/concepts/transforms.rst

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,16 @@ about the state of the tasks at given points. Here is an example:
105105

106106
.. code-block:: python
107107
108-
from voluptuous import Optional, Required
109-
108+
from typing import Optional
110109
from taskgraph.transforms.base import TransformSequence
111110
from taskgraph.util.schema import Schema
112111
113-
my_schema = Schema({
114-
Required("foo"): str,
115-
Optional("bar"): bool,
116-
})
112+
class MySchema(Schema):
113+
foo: str # Required field
114+
bar: Optional[bool] = None # Optional field
117115
118-
transforms.add_validate(my_schema)
116+
transforms = TransformSequence()
117+
transforms.add_validate(MySchema)
119118
120119
In the above example, we can be sure that every task dict has a string field
121120
called ``foo``, and may or may not have a boolean field called ``bar``.

docs/tutorials/creating-a-task-graph.rst

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,16 +136,16 @@ comments for explanations):
136136

137137
.. code-block:: python
138138
139-
from voluptuous import Optional, Required
140-
141-
from taskgraph.transforms.base import TransformSequence
139+
from typing import Optional
142140
from taskgraph.util.schema import Schema
141+
from taskgraph.transforms.base import TransformSequence
142+
143+
# Define the schema using Schema base class.
144+
class HelloDescriptionSchema(Schema):
145+
text: str # Required field
146+
description: Optional[str] = None # Optional field
143147
144-
# Define the schema. We use the `voluptuous` package to handle validation.
145-
hello_description_schema = Schema({
146-
Required("text"): str,
147-
Optional("description"): str,
148-
})
148+
hello_description_schema = HelloDescriptionSchema
149149
150150
# Create a 'TransformSequence' instance. This class collects transform
151151
# functions to run later.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ dependencies = [
2525
"cookiecutter~=2.1",
2626
"json-e>=2.7",
2727
"mozilla-repo-urls",
28+
"msgspec>=0.18.6",
2829
"PyYAML>=5.3.1",
2930
"redo>=2.0",
3031
"requests>=2.25",
3132
"slugid>=2.0",
3233
"taskcluster>=55.0",
3334
"taskcluster-urls>=11.0",
34-
"voluptuous>=0.12.1",
3535
]
3636

3737
[project.optional-dependencies]

src/taskgraph/config.py

Lines changed: 94 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -2,113 +2,112 @@
22
# License, v. 2.0. If a copy of the MPL was not distributed with this
33
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
44

5-
65
import logging
76
import os
87
import sys
98
from dataclasses import dataclass
109
from pathlib import Path
11-
from typing import Dict
12-
13-
from voluptuous import ALLOW_EXTRA, All, Any, Extra, Length, Optional, Required
10+
from typing import Dict, List, Literal, Optional, Union
1411

1512
from .util.caches import CACHES
1613
from .util.python_path import find_object
17-
from .util.schema import Schema, optionally_keyed_by, validate_schema
14+
from .util.schema import Schema, TaskPriority, optionally_keyed_by, validate_schema
1815
from .util.vcs import get_repository
1916
from .util.yaml import load_yaml
2017

2118
logger = logging.getLogger(__name__)
2219

20+
# CacheName type for valid cache names
21+
CacheName = Literal[tuple(CACHES.keys())]
22+
23+
24+
class WorkerAliasSchema(Schema):
25+
"""Worker alias configuration."""
26+
27+
provisioner: optionally_keyed_by("level", str) # type: ignore
28+
implementation: str
29+
os: str
30+
worker_type: optionally_keyed_by("level", str) # type: ignore
31+
32+
33+
class WorkersSchema(Schema, rename=None):
34+
"""Workers configuration."""
35+
36+
aliases: Dict[str, WorkerAliasSchema]
37+
38+
39+
class Repository(Schema, forbid_unknown_fields=False):
40+
"""Repository configuration.
41+
42+
This schema allows extra fields for repository-specific configuration.
43+
"""
44+
45+
# Required fields first
46+
name: str
47+
48+
# Optional fields
49+
project_regex: Optional[str] = None # Maps from "project-regex"
50+
ssh_secret_name: Optional[str] = None # Maps from "ssh-secret-name"
51+
52+
53+
class RunConfig(Schema):
54+
"""Run transforms configuration."""
55+
56+
# List of caches to enable, or a boolean to enable/disable all of them.
57+
use_caches: Optional[Union[bool, List[str]]] = None # Maps from "use-caches"
58+
59+
def __post_init__(self):
60+
"""Validate that cache names are valid."""
61+
if isinstance(self.use_caches, list):
62+
invalid = set(self.use_caches) - set(CACHES.keys())
63+
if invalid:
64+
raise ValueError(
65+
f"Invalid cache names: {invalid}. "
66+
f"Valid names are: {list(CACHES.keys())}"
67+
)
68+
69+
70+
class TaskGraphSchema(Schema):
71+
"""Taskgraph specific configuration."""
72+
73+
# Required fields first
74+
repositories: Dict[str, Repository]
75+
76+
# Optional fields
77+
# Python function to call to register extensions.
78+
register: Optional[str] = None
79+
decision_parameters: Optional[str] = None # Maps from "decision-parameters"
80+
# The taskcluster index prefix to use for caching tasks. Defaults to `trust-domain`.
81+
cached_task_prefix: Optional[str] = None # Maps from "cached-task-prefix"
82+
# Should tasks from pull requests populate the cache
83+
cache_pull_requests: Optional[bool] = None # Maps from "cache-pull-requests"
84+
# Regular expressions matching index paths to be summarized.
85+
index_path_regexes: Optional[List[str]] = None # Maps from "index-path-regexes"
86+
# Configuration related to the 'run' transforms.
87+
run: Optional[RunConfig] = None
88+
89+
90+
class GraphConfigSchema(Schema, forbid_unknown_fields=False):
91+
"""Main graph configuration schema.
92+
93+
This schema allows extra fields for flexibility in graph configuration.
94+
"""
95+
96+
# Required fields first
97+
# The trust-domain for this graph.
98+
# (See https://firefox-source-docs.mozilla.org/taskcluster/taskcluster/taskgraph.html#taskgraph-trust-domain)
99+
trust_domain: str # Maps from "trust-domain"
100+
task_priority: optionally_keyed_by("project", "level", TaskPriority) # type: ignore
101+
workers: WorkersSchema
102+
taskgraph: TaskGraphSchema
23103

24-
#: Schema for the graph config
25-
graph_config_schema = Schema(
26-
{
27-
# The trust-domain for this graph.
28-
# (See https://firefox-source-docs.mozilla.org/taskcluster/taskcluster/taskgraph.html#taskgraph-trust-domain) # noqa
29-
Required("trust-domain"): str,
30-
Optional(
31-
"docker-image-kind",
32-
description="Name of the docker image kind (default: docker-image)",
33-
): str,
34-
Required("task-priority"): optionally_keyed_by(
35-
"project",
36-
"level",
37-
Any(
38-
"highest",
39-
"very-high",
40-
"high",
41-
"medium",
42-
"low",
43-
"very-low",
44-
"lowest",
45-
),
46-
),
47-
Optional(
48-
"task-deadline-after",
49-
description="Default 'deadline' for tasks, in relative date format. "
50-
"Eg: '1 week'",
51-
): optionally_keyed_by("project", str),
52-
Optional(
53-
"task-expires-after",
54-
description="Default 'expires-after' for level 1 tasks, in relative date format. "
55-
"Eg: '90 days'",
56-
): str,
57-
Required("workers"): {
58-
Required("aliases"): {
59-
str: {
60-
Required("provisioner"): optionally_keyed_by("level", str),
61-
Required("implementation"): str,
62-
Required("os"): str,
63-
Required("worker-type"): optionally_keyed_by("level", str),
64-
}
65-
},
66-
},
67-
Required("taskgraph"): {
68-
Optional(
69-
"register",
70-
description="Python function to call to register extensions.",
71-
): str,
72-
Optional("decision-parameters"): str,
73-
Optional(
74-
"cached-task-prefix",
75-
description="The taskcluster index prefix to use for caching tasks. "
76-
"Defaults to `trust-domain`.",
77-
): str,
78-
Optional(
79-
"cache-pull-requests",
80-
description="Should tasks from pull requests populate the cache",
81-
): bool,
82-
Optional(
83-
"index-path-regexes",
84-
description="Regular expressions matching index paths to be summarized.",
85-
): [str],
86-
Optional(
87-
"run",
88-
description="Configuration related to the 'run' transforms.",
89-
): {
90-
Optional(
91-
"use-caches",
92-
description="List of caches to enable, or a boolean to "
93-
"enable/disable all of them.",
94-
): Any(bool, list(CACHES.keys())),
95-
},
96-
Required("repositories"): All(
97-
{
98-
str: {
99-
Required("name"): str,
100-
Optional("project-regex"): str,
101-
Optional("ssh-secret-name"): str,
102-
# FIXME
103-
Extra: str,
104-
}
105-
},
106-
Length(min=1),
107-
),
108-
},
109-
},
110-
extra=ALLOW_EXTRA,
111-
)
104+
# Optional fields
105+
# Name of the docker image kind (default: docker-image)
106+
docker_image_kind: Optional[str] = None # Maps from "docker-image-kind"
107+
# Default 'deadline' for tasks, in relative date format. Eg: '1 week'
108+
task_deadline_after: Optional[optionally_keyed_by("project", str)] = None # type: ignore
109+
# Default 'expires-after' for level 1 tasks, in relative date format. Eg: '90 days'
110+
task_expires_after: Optional[str] = None # Maps from "task-expires-after"
112111

113112

114113
@dataclass(frozen=True, eq=False)
@@ -179,7 +178,8 @@ def kinds_dir(self):
179178

180179

181180
def validate_graph_config(config):
182-
validate_schema(graph_config_schema, config, "Invalid graph configuration:")
181+
"""Validate graph configuration using msgspec."""
182+
validate_schema(GraphConfigSchema, config, "Invalid graph configuration:")
183183

184184

185185
def load_graph_config(root_dir):

src/taskgraph/decision.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
import shutil
1010
import time
1111
from pathlib import Path
12+
from typing import Any, Dict, Optional
1213

1314
import yaml
14-
from voluptuous import Optional
1515

1616
from taskgraph.actions import render_actions_json
1717
from taskgraph.create import create_tasks
@@ -40,11 +40,9 @@
4040

4141

4242
#: Schema for try_task_config.json version 2
43-
try_task_config_schema_v2 = Schema(
44-
{
45-
Optional("parameters"): {str: object},
46-
}
47-
)
43+
class TryTaskConfigSchemaV2(Schema):
44+
# All fields are optional
45+
parameters: Optional[Dict[str, Any]] = None
4846

4947

5048
def full_task_graph_to_runnable_tasks(full_task_json):
@@ -354,7 +352,7 @@ def set_try_config(parameters, task_config_file):
354352
task_config_version = task_config.pop("version")
355353
if task_config_version == 2:
356354
validate_schema(
357-
try_task_config_schema_v2,
355+
TryTaskConfigSchemaV2,
358356
task_config,
359357
"Invalid v2 `try_task_config.json`.",
360358
)

0 commit comments

Comments
 (0)