Skip to content

Commit 711a469

Browse files
authored
feat: add strict flag to package content validation (#62)
1 parent 416be32 commit 711a469

File tree

3 files changed

+178
-4
lines changed

3 files changed

+178
-4
lines changed

conda_recipe_v2_schema/model.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -500,9 +500,21 @@ class DownstreamTestElement(StrictBaseModel):
500500
)
501501

502502

503+
class FileChecks(StrictBaseModel):
504+
exists: ConditionalList[NonEmptyStr] | None = Field(
505+
default=[],
506+
description="Files or glob patterns that must exist anywhere inside the package.",
507+
)
508+
not_exists: ConditionalList[NonEmptyStr] | None = Field(
509+
default=[],
510+
description="Files or glob patterns that must NOT exist anywhere inside the package.",
511+
)
512+
513+
503514
class PackageContentTestInner(StrictBaseModel):
504-
files: ConditionalList[NonEmptyStr] | None = Field(
505-
default=[], description="Files that should be in the package"
515+
files: ConditionalList[NonEmptyStr] | FileChecks | None = Field(
516+
default=None,
517+
description="Files expectations for the whole package. Can be a list of files/globs or an object with exists/not_exists.",
506518
)
507519
include: ConditionalList[NonEmptyStr] | None = Field(
508520
default=[],
@@ -520,6 +532,10 @@ class PackageContentTestInner(StrictBaseModel):
520532
default=[],
521533
description="Files that should be in the `lib/` folder of the package. This folder is found under `$PREFIX/lib` on Unix and %PREFIX%/Library/lib on Windows.",
522534
)
535+
strict: bool = Field(
536+
default=False,
537+
description="When true, the package must not contain any files other than those specified.",
538+
)
523539

524540

525541
class PackageContentTest(StrictBaseModel):

schema.json

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,6 +1227,75 @@
12271227
"title": "DynamicLinking",
12281228
"type": "object"
12291229
},
1230+
"FileChecks": {
1231+
"additionalProperties": false,
1232+
"properties": {
1233+
"exists": {
1234+
"anyOf": [
1235+
{
1236+
"minLength": 1,
1237+
"type": "string"
1238+
},
1239+
{
1240+
"$ref": "#/$defs/IfStatement"
1241+
},
1242+
{
1243+
"items": {
1244+
"anyOf": [
1245+
{
1246+
"minLength": 1,
1247+
"type": "string"
1248+
},
1249+
{
1250+
"$ref": "#/$defs/IfStatement"
1251+
}
1252+
]
1253+
},
1254+
"type": "array"
1255+
},
1256+
{
1257+
"type": "null"
1258+
}
1259+
],
1260+
"default": [],
1261+
"description": "Files or glob patterns that must exist anywhere inside the package.",
1262+
"title": "Exists"
1263+
},
1264+
"not_exists": {
1265+
"anyOf": [
1266+
{
1267+
"minLength": 1,
1268+
"type": "string"
1269+
},
1270+
{
1271+
"$ref": "#/$defs/IfStatement"
1272+
},
1273+
{
1274+
"items": {
1275+
"anyOf": [
1276+
{
1277+
"minLength": 1,
1278+
"type": "string"
1279+
},
1280+
{
1281+
"$ref": "#/$defs/IfStatement"
1282+
}
1283+
]
1284+
},
1285+
"type": "array"
1286+
},
1287+
{
1288+
"type": "null"
1289+
}
1290+
],
1291+
"default": [],
1292+
"description": "Files or glob patterns that must NOT exist anywhere inside the package.",
1293+
"title": "Not Exists"
1294+
}
1295+
},
1296+
"title": "FileChecks",
1297+
"type": "object"
1298+
},
12301299
"FileScript": {
12311300
"additionalProperties": false,
12321301
"properties": {
@@ -2945,12 +3014,15 @@
29453014
},
29463015
"type": "array"
29473016
},
3017+
{
3018+
"$ref": "#/$defs/FileChecks"
3019+
},
29483020
{
29493021
"type": "null"
29503022
}
29513023
],
2952-
"default": [],
2953-
"description": "Files that should be in the package",
3024+
"default": null,
3025+
"description": "Files expectations for the whole package. Can be a list of files/globs or an object with exists/not_exists.",
29543026
"title": "Files"
29553027
},
29563028
"include": {
@@ -3076,6 +3148,12 @@
30763148
"default": [],
30773149
"description": "Files that should be in the `lib/` folder of the package. This folder is found under `$PREFIX/lib` on Unix and %PREFIX%/Library/lib on Windows.",
30783150
"title": "Lib"
3151+
},
3152+
"strict": {
3153+
"default": false,
3154+
"description": "When true, the package must not contain any files other than those specified.",
3155+
"title": "Strict",
3156+
"type": "boolean"
30793157
}
30803158
},
30813159
"title": "PackageContentTestInner",

tests/test_recipy.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,83 @@ def test_r_test_invalid_missing_libraries():
148148
recipe_dict = yaml.safe_load(recipe_yaml)
149149
with pytest.raises(PydanticValidationError):
150150
Recipe.validate_python(recipe_dict)
151+
152+
153+
def test_package_contents_strict_valid(recipe_schema):
154+
"""Recipes with a boolean strict flag should validate successfully."""
155+
for strict_val in (True, False):
156+
recipe_yaml = f"""
157+
package:
158+
name: test
159+
version: 1.0.0
160+
tests:
161+
- package_contents:
162+
strict: {str(strict_val).lower()}
163+
files:
164+
- foo.txt
165+
"""
166+
recipe_dict = yaml.safe_load(recipe_yaml)
167+
168+
Recipe.validate_python(recipe_dict)
169+
170+
validate(instance=recipe_dict, schema=recipe_schema)
171+
172+
173+
def test_package_contents_strict_invalid_type(recipe_schema):
174+
"""Non-boolean values for strict should fail validation."""
175+
recipe_yaml = """
176+
package:
177+
name: test
178+
version: 1.0.0
179+
tests:
180+
- package_contents:
181+
strict: 123
182+
"""
183+
recipe_dict = yaml.safe_load(recipe_yaml)
184+
185+
with pytest.raises(PydanticValidationError):
186+
Recipe.validate_python(recipe_dict)
187+
188+
with pytest.raises(ValidationError):
189+
validate(instance=recipe_dict, schema=recipe_schema)
190+
191+
192+
def test_package_contents_exists_and_not_exists_valid(recipe_schema):
193+
"""Recipes using files.exists / files.not_exists should validate successfully."""
194+
recipe_yaml = """
195+
package:
196+
name: test
197+
version: 1.0.0
198+
tests:
199+
- package_contents:
200+
files:
201+
exists:
202+
- bar.txt
203+
not_exists:
204+
- secret.key
205+
- '*.pem'
206+
"""
207+
recipe_dict = yaml.safe_load(recipe_yaml)
208+
209+
Recipe.validate_python(recipe_dict)
210+
validate(instance=recipe_dict, schema=recipe_schema)
211+
212+
213+
def test_package_contents_exists_invalid_type(recipe_schema):
214+
"""Non-string/non-list values for exists should fail validation."""
215+
recipe_yaml = """
216+
package:
217+
name: test
218+
version: 1.0.0
219+
tests:
220+
- package_contents:
221+
files:
222+
exists: 123
223+
"""
224+
recipe_dict = yaml.safe_load(recipe_yaml)
225+
226+
with pytest.raises(PydanticValidationError):
227+
Recipe.validate_python(recipe_dict)
228+
229+
with pytest.raises(ValidationError):
230+
validate(instance=recipe_dict, schema=recipe_schema)

0 commit comments

Comments
 (0)