Skip to content

Commit 050c886

Browse files
authored
Merge pull request #9016 from readthedocs/humitos/build-jobs-config-object
2 parents 8ea3507 + 5d08d4e commit 050c886

File tree

5 files changed

+260
-99
lines changed

5 files changed

+260
-99
lines changed

readthedocs/config/config.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .find import find_one
1616
from .models import (
1717
Build,
18+
BuildJobs,
1819
BuildTool,
1920
BuildWithTools,
2021
Conda,
@@ -751,6 +752,10 @@ def validate_conda(self):
751752
conda['environment'] = validate_path(environment, self.base_path)
752753
return conda
753754

755+
# NOTE: I think we should rename `BuildWithTools` to `BuildWithOs` since
756+
# `os` is the main and mandatory key that makes the diference
757+
#
758+
# NOTE: `build.jobs` can't be used without using `build.os`
754759
def validate_build_config_with_tools(self):
755760
"""
756761
Validates the build object (new format).
@@ -769,6 +774,22 @@ def validate_build_config_with_tools(self):
769774
for tool in tools.keys():
770775
validate_choice(tool, self.settings['tools'].keys())
771776

777+
jobs = {}
778+
with self.catch_validation_error("build.jobs"):
779+
# FIXME: should we use `default={}` or kept the `None` here and
780+
# shortcircuit the rest of the logic?
781+
jobs = self.pop_config("build.jobs", default={})
782+
validate_dict(jobs)
783+
# NOTE: besides validating that each key is one of the expected
784+
# ones, we could validate the value of each of them is a list of
785+
# commands. However, I don't think we should validate the "command"
786+
# looks like a real command.
787+
for job in jobs.keys():
788+
validate_choice(
789+
job,
790+
BuildJobs.__slots__,
791+
)
792+
772793
if not tools:
773794
self.error(
774795
key='build.tools',
@@ -780,6 +801,13 @@ def validate_build_config_with_tools(self):
780801
code=CONFIG_REQUIRED,
781802
)
782803

804+
build["jobs"] = {}
805+
for job, commands in jobs.items():
806+
with self.catch_validation_error(f"build.jobs.{job}"):
807+
build["jobs"][job] = [
808+
validate_string(command) for command in validate_list(commands)
809+
]
810+
783811
build['tools'] = {}
784812
for tool, version in tools.items():
785813
with self.catch_validation_error(f'build.tools.{tool}'):
@@ -1263,7 +1291,8 @@ def build(self):
12631291
return BuildWithTools(
12641292
os=build['os'],
12651293
tools=tools,
1266-
apt_packages=build['apt_packages'],
1294+
jobs=BuildJobs(**build["jobs"]),
1295+
apt_packages=build["apt_packages"],
12671296
)
12681297
return Build(**build)
12691298

readthedocs/config/models.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def __init__(self, **kwargs):
3737

3838
class BuildWithTools(Base):
3939

40-
__slots__ = ('os', 'tools', 'apt_packages')
40+
__slots__ = ("os", "tools", "jobs", "apt_packages")
4141

4242
def __init__(self, **kwargs):
4343
kwargs.setdefault('apt_packages', [])
@@ -49,6 +49,35 @@ class BuildTool(Base):
4949
__slots__ = ('version', 'full_version')
5050

5151

52+
class BuildJobs(Base):
53+
54+
"""Object used for `build.jobs` key."""
55+
56+
__slots__ = (
57+
"pre_checkout",
58+
"post_checkout",
59+
"pre_system_dependencies",
60+
"post_system_dependencies",
61+
"pre_create_environment",
62+
"post_create_environment",
63+
"pre_install",
64+
"post_install",
65+
"pre_build",
66+
"post_build",
67+
)
68+
69+
def __init__(self, **kwargs):
70+
"""
71+
Create an empty list as a default for all possible builds.jobs configs.
72+
73+
This is necessary because it makes the code cleaner when we add items to these lists,
74+
without having to check for a dict to be created first.
75+
"""
76+
for step in self.__slots__:
77+
kwargs.setdefault(step, [])
78+
super().__init__(**kwargs)
79+
80+
5281
class Python(Base):
5382

5483
__slots__ = ('version', 'install', 'use_system_site_packages')

readthedocs/config/tests/test_config.py

+95-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import os
2-
from django.conf import settings
32
import re
43
import textwrap
54
from collections import OrderedDict
65
from unittest.mock import DEFAULT, patch
76

87
import pytest
8+
from django.conf import settings
99
from pytest import raises
1010

1111
from readthedocs.config import (
@@ -33,6 +33,7 @@
3333
)
3434
from readthedocs.config.models import (
3535
Build,
36+
BuildJobs,
3637
BuildWithTools,
3738
Conda,
3839
PythonInstall,
@@ -1012,6 +1013,87 @@ def test_new_build_config_conflict_with_build_python_version(self):
10121013
build.validate()
10131014
assert excinfo.value.key == 'python.version'
10141015

1016+
@pytest.mark.parametrize("value", ["", None, "pre_invalid"])
1017+
def test_jobs_build_config_invalid_jobs(self, value):
1018+
build = self.get_build_config(
1019+
{
1020+
"build": {
1021+
"os": "ubuntu-20.04",
1022+
"tools": {"python": "3.8"},
1023+
"jobs": {value: ["echo 1234", "git fetch --unshallow"]},
1024+
},
1025+
},
1026+
)
1027+
with raises(InvalidConfig) as excinfo:
1028+
build.validate()
1029+
assert excinfo.value.key == "build.jobs"
1030+
1031+
@pytest.mark.parametrize("value", ["", None, "echo 123", 42])
1032+
def test_jobs_build_config_invalid_job_commands(self, value):
1033+
build = self.get_build_config(
1034+
{
1035+
"build": {
1036+
"os": "ubuntu-20.04",
1037+
"tools": {"python": "3.8"},
1038+
"jobs": {
1039+
"pre_install": value,
1040+
},
1041+
},
1042+
},
1043+
)
1044+
with raises(InvalidConfig) as excinfo:
1045+
build.validate()
1046+
assert excinfo.value.key == "build.jobs.pre_install"
1047+
1048+
def test_jobs_build_config(self):
1049+
build = self.get_build_config(
1050+
{
1051+
"build": {
1052+
"os": "ubuntu-20.04",
1053+
"tools": {"python": "3.8"},
1054+
"jobs": {
1055+
"pre_checkout": ["echo pre_checkout"],
1056+
"post_checkout": ["echo post_checkout"],
1057+
"pre_system_dependencies": ["echo pre_system_dependencies"],
1058+
"post_system_dependencies": ["echo post_system_dependencies"],
1059+
"pre_create_environment": ["echo pre_create_environment"],
1060+
"post_create_environment": ["echo post_create_environment"],
1061+
"pre_install": ["echo pre_install", "echo `date`"],
1062+
"post_install": ["echo post_install"],
1063+
"pre_build": [
1064+
"echo pre_build",
1065+
'sed -i -e "s|{VERSION}|${READTHEDOCS_VERSION_NAME}|g"',
1066+
],
1067+
"post_build": ["echo post_build"],
1068+
},
1069+
},
1070+
},
1071+
)
1072+
build.validate()
1073+
assert isinstance(build.build, BuildWithTools)
1074+
assert isinstance(build.build.jobs, BuildJobs)
1075+
assert build.build.jobs.pre_checkout == ["echo pre_checkout"]
1076+
assert build.build.jobs.post_checkout == ["echo post_checkout"]
1077+
assert build.build.jobs.pre_system_dependencies == [
1078+
"echo pre_system_dependencies"
1079+
]
1080+
assert build.build.jobs.post_system_dependencies == [
1081+
"echo post_system_dependencies"
1082+
]
1083+
assert build.build.jobs.pre_create_environment == [
1084+
"echo pre_create_environment"
1085+
]
1086+
assert build.build.jobs.post_create_environment == [
1087+
"echo post_create_environment"
1088+
]
1089+
assert build.build.jobs.pre_install == ["echo pre_install", "echo `date`"]
1090+
assert build.build.jobs.post_install == ["echo post_install"]
1091+
assert build.build.jobs.pre_build == [
1092+
"echo pre_build",
1093+
'sed -i -e "s|{VERSION}|${READTHEDOCS_VERSION_NAME}|g"',
1094+
]
1095+
assert build.build.jobs.post_build == ["echo post_build"]
1096+
10151097
@pytest.mark.parametrize(
10161098
'value',
10171099
[
@@ -2297,6 +2379,18 @@ def test_as_dict_new_build_config(self, tmpdir):
22972379
'full_version': settings.RTD_DOCKER_BUILD_SETTINGS['tools']['nodejs']['16'],
22982380
},
22992381
},
2382+
"jobs": {
2383+
"pre_checkout": [],
2384+
"post_checkout": [],
2385+
"pre_system_dependencies": [],
2386+
"post_system_dependencies": [],
2387+
"pre_create_environment": [],
2388+
"post_create_environment": [],
2389+
"pre_install": [],
2390+
"post_install": [],
2391+
"pre_build": [],
2392+
"post_build": [],
2393+
},
23002394
'apt_packages': [],
23012395
},
23022396
'conda': None,

0 commit comments

Comments
 (0)