• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 22981081902

12 Mar 2026 12:31AM UTC coverage: 92.932% (-0.006%) from 92.938%
22981081902

Pull #23165

github

web-flow
Merge a62cf2b59 into 819035d5d
Pull Request #23165: [pants_ng] An NG subsystem implementation

269 of 295 new or added lines in 2 files covered. (91.19%)

1 existing line in 1 file now uncovered.

91437 of 98391 relevant lines covered (92.93%)

4.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.44
/src/python/pants/ng/subsystem_test.py
1
# Copyright 2025 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
import re
1✔
5
import textwrap
1✔
6

7
import pytest
1✔
8

9
from pants.engine.internals.native_engine import PyConfigSource, PyNgOptionsReader
1✔
10
from pants.ng.subsystem import OptionDescriptor, SubsystemNg, option
1✔
11
from pants.util.frozendict import FrozenDict
1✔
12

13

14
def test_collect_options() -> None:
1✔
15
    class NoOptionsScope(SubsystemNg):
1✔
16
        pass
1✔
17

18
    with pytest.raises(
1✔
19
        ValueError,
20
        match="Subsystem class NoOptionsScope must set the options_scope classvar.",
21
    ):
22
        NoOptionsScope._initialize_()
1✔
23

24
    class NoHelp(SubsystemNg):
1✔
25
        options_scope = "no_help"
1✔
26

27
    with pytest.raises(
1✔
28
        ValueError,
29
        match="Subsystem class NoHelp must set the help classvar.",
30
    ):
31
        NoHelp._initialize_()
1✔
32

33
    class Empty(SubsystemNg):
1✔
34
        options_scope = "empty"
1✔
35
        help = "empty help"
1✔
36

37
        def not_an_option(self) -> str:
1✔
NEW
38
            return ""
×
39

40
    assert getattr(Empty, "_option_descriptors_", None) is None
1✔
41
    Empty._initialize_()
1✔
42
    assert getattr(Empty, "_option_descriptors_") == tuple()
1✔
43

44
    class SomeOptions(SubsystemNg):
1✔
45
        options_scope = "some_options"
1✔
46
        help = "some_options help"
1✔
47

48
        @option(help="foo help")
49
        def foo(self) -> str: ...
50

51
        @option(default=42, help="bar help")
52
        def bar(self) -> int: ...
53

54
        def not_an_option(self) -> str:
1✔
NEW
55
            return ""
×
56

57
        @option(help="baz help")
58
        def baz(self) -> bool: ...
59

60
        @option(default=3.14, help="qux help")
61
        def qux(self) -> float: ...
62

63
        @option(help="str tuple help")
64
        def str_tuple(self) -> tuple[str, ...]: ...
65

66
        @option(help="int tuple help", default=(0, 1, 2))
67
        def int_tuple(self) -> tuple[int, ...]: ...
68

69
        @option(help="dict help")
70
        def dict(self) -> FrozenDict[str, str]: ...
71

72
    assert getattr(SomeOptions, "_option_descriptors_", None) is None
1✔
73
    SomeOptions._initialize_()
1✔
74
    assert getattr(SomeOptions, "_option_descriptors_") == (
1✔
75
        OptionDescriptor("foo", str, None, "foo help"),
76
        OptionDescriptor("bar", int, 42, "bar help"),
77
        OptionDescriptor("baz", bool, None, "baz help"),
78
        OptionDescriptor("qux", float, 3.14, "qux help"),
79
        OptionDescriptor("str_tuple", tuple[str, ...], None, "str tuple help"),
80
        OptionDescriptor("int_tuple", tuple[int, ...], (0, 1, 2), "int tuple help"),
81
        OptionDescriptor("dict", FrozenDict[str, str], None, "dict help"),
82
    )
83

84
    class NotAMethod(SubsystemNg):
1✔
85
        options_scope = "not_a_method"
1✔
86
        help = "not_a_method help"
1✔
87

88
        @option(help="not a method")
1✔
89
        class Inner:
1✔
90
            pass
1✔
91

92
    with pytest.raises(
1✔
93
        ValueError,
94
        match="The @option decorator expects to be placed on a no-arg instance method, but "
95
        "NotAMethod.Inner does not have this signature.",
96
    ):
97
        NotAMethod._initialize_()
1✔
98

99
    class NoSelfArg(SubsystemNg):
1✔
100
        options_scope = "no_self_arg"
1✔
101
        help = "no_self_arg help"
1✔
102

103
        @option(help="no self arg")
104
        def no_self_arg() -> str: ...
105

106
    with pytest.raises(
1✔
107
        ValueError,
108
        match="The @option decorator expects to be placed on a no-arg instance method, but "
109
        "NoSelfArg.no_self_arg does not have this signature.",
110
    ):
111
        NoSelfArg._initialize_()
1✔
112

113
    class BadDefault(SubsystemNg):
1✔
114
        options_scope = "bad_default"
1✔
115
        help = "bad_default help"
1✔
116

117
        @option(default=42, help="bad default")
118
        def bad_default(self) -> str: ...
119

120
    with pytest.raises(
1✔
121
        ValueError,
122
        match=re.escape(
123
            r"The default for option BadDefault.bad_default must be of type str (or None)."
124
        ),
125
    ):
126
        BadDefault._initialize_()
1✔
127

128
    class BadTupleDefault(SubsystemNg):
1✔
129
        options_scope = "bad_tuple_default"
1✔
130
        help = "bad_tuple_default help"
1✔
131

132
        @option(default=(0, "1", 2), help="bad tuple default")
133
        def bad_default(self) -> tuple[int, ...]: ...
134

135
    with pytest.raises(
1✔
136
        ValueError,
137
        match=re.escape(
138
            r"The default for option BadTupleDefault.bad_default must be of type tuple[int, ...] (or None)."
139
        ),
140
    ):
141
        BadTupleDefault._initialize_()
1✔
142

143

144
def test_option_value_getters_implicit_defaults(tmp_path) -> None:
1✔
145
    class Dummy(SubsystemNg):
1✔
146
        options_scope = "dummy"
1✔
147
        help = "dummy help"
1✔
148

149
        @option(help="bool help")
150
        def bool_opt(self) -> bool: ...
151

152
        @option(help="str help")
153
        def str_opt(self) -> str: ...
154

155
        @option(help="int help")
156
        def int_opt(self) -> int: ...
157

158
        @option(help="float help")
159
        def float_opt(self) -> float: ...
160

161
        @option(help="bool tuple help")
162
        def bool_tuple_opt(self) -> tuple[bool, ...]: ...
163

164
        @option(help="str tuple help")
165
        def str_tuple_opt(self) -> tuple[str, ...]: ...
166

167
        @option(help="int tuple help")
168
        def int_tuple_opt(self) -> tuple[int, ...]: ...
169

170
        @option(help="float tuple help")
171
        def float_tuple_opt(self) -> tuple[float, ...]: ...
172

173
        @option(help="frozendict help")
174
        def frozendict_opt(self) -> FrozenDict[str, str]: ...
175

176
    Dummy._initialize_()
1✔
177

178
    subsys = Dummy.create(
1✔
179
        PyNgOptionsReader(
180
            buildroot=tmp_path,
181
            flags={},
182
            env={},
183
            configs=[],
184
        )
185
    )
186

187
    assert not subsys.bool_opt
1✔
188
    assert subsys.str_opt is None
1✔
189
    assert subsys.int_opt is None
1✔
190
    assert subsys.float_opt is None
1✔
191
    assert subsys.bool_tuple_opt == tuple()
1✔
192
    assert subsys.str_tuple_opt == tuple()
1✔
193
    assert subsys.int_tuple_opt == tuple()
1✔
194
    assert subsys.float_tuple_opt == tuple()
1✔
195
    assert subsys.frozendict_opt == FrozenDict()
1✔
196

197

198
def test_option_value_getters_explicit_defaults(tmp_path) -> None:
1✔
199
    class Dummy(SubsystemNg):
1✔
200
        options_scope = "dummy"
1✔
201
        help = "dummy help"
1✔
202

203
        @option(default=True, help="bool help")
204
        def bool_opt(self) -> bool: ...
205

206
        @option(default="hello world", help="str help")
207
        def str_opt(self) -> str: ...
208

209
        @option(default=42, help="int help")
210
        def int_opt(self) -> int: ...
211

212
        @option(default=3.14, help="float help")
213
        def float_opt(self) -> float: ...
214

215
        @option(default=(True, False), help="bool tuple help")
216
        def bool_tuple_opt(self) -> tuple[bool, ...]: ...
217

218
        @option(default=("hello", "world"), help="str tuple help")
219
        def str_tuple_opt(self) -> tuple[str, ...]: ...
220

221
        @option(default=(0, 1, 2), help="int tuple help")
222
        def int_tuple_opt(self) -> tuple[int, ...]: ...
223

224
        @option(default=(0.1, 1.2, 2.3), help="float tuple help")
225
        def float_tuple_opt(self) -> tuple[float, ...]: ...
226

227
        @option(default=FrozenDict(foo="bar"), help="frozendict help")
228
        def frozendict_opt(self) -> FrozenDict[str, str]: ...
229

230
    Dummy._initialize_()
1✔
231

232
    subsys = Dummy.create(
1✔
233
        PyNgOptionsReader(
234
            buildroot=tmp_path,
235
            flags={},
236
            env={},
237
            configs=[],
238
        )
239
    )
240

241
    assert subsys.bool_opt
1✔
242
    assert subsys.str_opt == "hello world"
1✔
243
    assert subsys.int_opt == 42
1✔
244
    assert subsys.float_opt == 3.14
1✔
245
    assert subsys.bool_tuple_opt == (True, False)
1✔
246
    assert subsys.str_tuple_opt == ("hello", "world")
1✔
247
    assert subsys.int_tuple_opt == (0, 1, 2)
1✔
248
    assert subsys.float_tuple_opt == (0.1, 1.2, 2.3)
1✔
249
    assert subsys.frozendict_opt == FrozenDict(foo="bar")
1✔
250

251

252
def test_option_callable_args(tmp_path) -> None:
1✔
253
    class Dummy(SubsystemNg):
1✔
254
        options_scope = "dummy"
1✔
255
        help = "dummy help"
1✔
256

257
        _bool_opt_help = "bool help"
1✔
258

259
        @option(default=True, help=lambda cls: cls._bool_opt_help)
260
        def bool_opt(self) -> bool: ...
261

262
        _str_default = "hello world"
1✔
263

264
        @option(default=lambda cls: cls._str_default, help="str help")
265
        def str_opt(self) -> str: ...
266

267
    Dummy._initialize_()
1✔
268

269
    assert getattr(Dummy, "_option_descriptors_") == (
1✔
270
        OptionDescriptor("bool_opt", bool, True, "bool help"),
271
        OptionDescriptor("str_opt", str, "hello world", "str help"),
272
    )
273

274

275
def test_option_value_getters_from_options(tmp_path) -> None:
1✔
276
    class Dummy(SubsystemNg):
1✔
277
        options_scope = "dummy"
1✔
278
        help = "dummy help"
1✔
279

280
        @option(default=True, help="bool help")
281
        def bool_opt(self) -> bool: ...
282

283
        @option(help="str help")
284
        def str_opt(self) -> str: ...
285

286
        @option(default=42, help="int help")
287
        def int_opt(self) -> int: ...
288

289
        @option(help="float help")
290
        def float_opt(self) -> float: ...
291

292
        @option(help="bool tuple help")
293
        def bool_tuple_opt(self) -> tuple[bool, ...]: ...
294

295
        @option(help="str tuple help")
296
        def str_tuple_opt(self) -> tuple[str, ...]: ...
297

298
        @option(default=(0, 1, 2), help="int tuple help")
299
        def int_tuple_opt(self) -> tuple[int, ...]: ...
300

301
        @option(default=(1.2, 2.3), help="float tuple help")
302
        def float_tuple_opt(self) -> tuple[float, ...]: ...
303

304
        @option(default=FrozenDict(foo="bar"), help="frozendict help")
305
        def frozendict_opt(self) -> FrozenDict[str, str]: ...
306

307
    Dummy._initialize_()
1✔
308

309
    # Options parsing is comprehensively tested elswhere, so we just spot-check
310
    # the plumbing here.
311

312
    config = textwrap.dedent("""\
1✔
313
    [dummy]
314
    bool_opt = false
315
    str_opt = "hello world"
316
    int_opt = 53
317
    float_opt = 5.6
318
    bool_tuple_opt = [true, false]
319
    str_tuple_opt = ["hello", "world"]
320
    int_tuple_opt.add = [3, 4, 5]
321
    float_tuple_opt = []
322
    frozendict_opt.add = {baz="qux"}
323
    """)
324
    config_source = PyConfigSource("pantsng.toml", config.encode())
1✔
325

326
    subsys = Dummy.create(
1✔
327
        PyNgOptionsReader(
328
            buildroot=tmp_path,
329
            flags={},
330
            env={},
331
            configs=[config_source],
332
        )
333
    )
334

335
    assert not subsys.bool_opt
1✔
336
    assert subsys.str_opt == "hello world"
1✔
337
    assert subsys.int_opt == 53
1✔
338
    assert subsys.float_opt == 5.6
1✔
339
    assert subsys.bool_tuple_opt == (True, False)
1✔
340
    assert subsys.str_tuple_opt == ("hello", "world")
1✔
341
    assert subsys.int_tuple_opt == (0, 1, 2, 3, 4, 5)
1✔
342
    assert subsys.float_tuple_opt == tuple()
1✔
343
    assert subsys.frozendict_opt == FrozenDict(foo="bar", baz="qux")
1✔
344

345

346
def test_required_options(tmp_path) -> None:
1✔
347
    class Dummy1(SubsystemNg):
1✔
348
        options_scope = "dummy1"
1✔
349
        help = "dummy1 help"
1✔
350

351
        @option(required=True, help="bool help")
352
        def bool_opt(self) -> bool: ...
353

354
    Dummy1._initialize_()
1✔
355

356
    with pytest.raises(
1✔
357
        ValueError, match=re.escape(r"No value provided for required option [dummy1].bool_opt.")
358
    ):
359
        Dummy1.create(
1✔
360
            PyNgOptionsReader(
361
                buildroot=tmp_path,
362
                flags={},
363
                env={},
364
                configs=[],
365
            )
366
        )
367

368
    class Dummy2(SubsystemNg):
1✔
369
        options_scope = "dummy2"
1✔
370
        help = "dummy2 help"
1✔
371

372
        @option(required=True, help="str tuple help")
373
        def str_tuple_opt(self) -> tuple[str, ...]: ...
374

375
    Dummy2._initialize_()
1✔
376

377
    with pytest.raises(
1✔
378
        ValueError,
379
        match=re.escape(r"No value provided for required option [dummy2].str_tuple_opt."),
380
    ):
381
        Dummy2.create(
1✔
382
            PyNgOptionsReader(
383
                buildroot=tmp_path,
384
                flags={},
385
                env={},
386
                configs=[],
387
            )
388
        )
389

390
    class Dummy3(SubsystemNg):
1✔
391
        options_scope = "dummy3"
1✔
392
        help = "dummy3 help"
1✔
393

394
        @option(required=True, help="dict help")
395
        def dict_opt(self) -> FrozenDict[str, str]: ...
396

397
    Dummy3._initialize_()
1✔
398

399
    with pytest.raises(
1✔
400
        ValueError, match=re.escape(r"No value provided for required option [dummy3].dict_opt.")
401
    ):
402
        Dummy3.create(
1✔
403
            PyNgOptionsReader(
404
                buildroot=tmp_path,
405
                flags={},
406
                env={},
407
                configs=[],
408
            )
409
        )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc