Skip to content

Commit c989fab

Browse files
authored
Merge pull request #261 from 23andMe/feature/add-exclude
Add --exclude argument to CLI
2 parents b44ed27 + a90c7e8 commit c989fab

File tree

5 files changed

+115
-38
lines changed

5 files changed

+115
-38
lines changed

README.md

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
Yamale (ya·ma·lē)
22
=================
3+
[![Build Status](https://github.com/23andMe/Yamale/actions/workflows/run-tests.yml/badge.svg)](https://github.com/23andMe/Yamale/actions/workflows/run-tests.yml)
4+
[![PyPI](https://img.shields.io/pypi/v/yamale.svg)](https://pypi.python.org/pypi/yamale)
5+
[![downloads](https://static.pepy.tech/badge/yamale/month)](https://pepy.tech/project/yamale)
6+
[![versions](https://img.shields.io/pypi/pyversions/yamale.svg)](https://github.com/yamale/yamale)
7+
[![license](https://img.shields.io/github/license/23andMe/yamale.svg)](https://github.com/23andMe/Yamale/blob/master/LICENSE)
38

49
| :warning: Ensure that your schema definitions come from internal or trusted sources. Yamale does not protect against intentionally malicious schemas. |
510
|:------------|
@@ -11,8 +16,6 @@ A schema and validator for YAML.
1116
What's YAML? See the current spec [here](http://www.yaml.org/spec/1.2/spec.html) and an introduction
1217
to the syntax [here](https://github.com/Animosity/CraftIRC/wiki/Complete-idiot's-introduction-to-yaml).
1318

14-
[![Build Status](https://github.com/23andMe/Yamale/actions/workflows/run-tests.yml/badge.svg)](https://github.com/23andMe/Yamale/actions/workflows/run-tests.yml)
15-
[![PyPI](https://img.shields.io/pypi/v/yamale.svg)](https://pypi.python.org/pypi/yamale)
1619

1720
Requirements
1821
------------
@@ -23,8 +26,10 @@ Requirements
2326
Install
2427
-------
2528
### pip
26-
```bash
29+
```
2730
$ pip install yamale
31+
# or to include ruamel.yaml as a dependency
32+
$ pip install yamale[ruamel]
2833
```
2934

3035
NOTE: Some platforms, e.g., Mac OS, may ship with only Python 2 and may not have pip installed.
@@ -49,25 +54,28 @@ looking up the directory tree until it finds one. If Yamale can not find a schem
4954

5055
Usage:
5156

52-
```bash
53-
usage: yamale [-h] [-s SCHEMA] [-n CPU_NUM] [-p PARSER] [--no-strict] [PATH]
57+
```
58+
usage: yamale [-h] [-s SCHEMA] [-e PATTERN] [-p PARSER] [-n CPU_NUM] [-x] [-v] [-V] [PATH ...]
5459
5560
Validate yaml files.
5661
5762
positional arguments:
58-
PATH folder to validate. Default is current directory.
63+
PATH Paths to validate, either directories or files. Default is the current directory.
5964
60-
optional arguments:
65+
options:
6166
-h, --help show this help message and exit
6267
-s SCHEMA, --schema SCHEMA
6368
filename of schema. Default is schema.yaml.
64-
-n CPU_NUM, --cpu-num CPU_NUM
65-
number of CPUs to use. Default is 4.
69+
-e PATTERN, --exclude PATTERN
70+
Python regex used to exclude files from validation. Any substring match of a file's absolute path will be excluded. Uses
71+
default Python3 regex. Option can be supplied multiple times.
6672
-p PARSER, --parser PARSER
67-
YAML library to load files. Choices are "ruamel" or
68-
"pyyaml" (default).
69-
--no-strict Disable strict mode, unexpected elements in the data
70-
will be accepted.
73+
YAML library to load files. Choices are "ruamel" or "pyyaml" (default).
74+
-n CPU_NUM, --cpu-num CPU_NUM
75+
Number of child processes to spawn for validation. Default is 4. 'auto' to use CPU count.
76+
-x, --no-strict Disable strict mode, unexpected elements in the data will be accepted.
77+
-v, --verbose show verbose information
78+
-V, --version show program's version number and exit
7179
```
7280

7381
### API

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
packages=find_packages(),
1919
include_package_data=True,
2020
install_requires=["pyyaml"],
21-
extras_requires={"ruamel": ["ruamel.yaml"]},
21+
extras_require={"ruamel": ["ruamel.yaml"]},
2222
python_requires=">=3.8",
2323
entry_points={
2424
"console_scripts": ["yamale=yamale.command_line:main"],

yamale/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
5.3.0
1+
6.0.0

yamale/command_line.py

Lines changed: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
import argparse
1212
import glob
1313
import os
14-
from multiprocessing import Pool
14+
import re
15+
import multiprocessing
1516
from .yamale_error import YamaleError
1617
from .schema.validationresults import Result
1718
from .version import __version__
@@ -64,31 +65,32 @@ def _find_schema(data_path, schema_name):
6465
return _find_data_path_schema(data_path, schema_name)
6566

6667

67-
def _validate_single(yaml_path, schema_name, parser, strict):
68-
print("Validating %s..." % yaml_path)
68+
def _validate_file(yaml_path, schema_name, parser, strict, should_exclude):
69+
if should_exclude(yaml_path):
70+
return
6971
s = _find_schema(yaml_path, schema_name)
7072
if not s:
7173
raise ValueError("Invalid schema name for '{}' or schema not found.".format(schema_name))
7274
_validate(s, yaml_path, parser, strict, True)
7375

7476

75-
def _validate_dir(root, schema_name, cpus, parser, strict):
76-
pool = Pool(processes=cpus)
77+
def _validate_dir(root, schema_name, cpus, parser, strict, should_exclude):
78+
pool = multiprocessing.Pool(processes=cpus)
7779
res = []
7880
error_messages = []
79-
print("Finding yaml files...")
80-
for root, dirs, files in os.walk(root):
81+
for root, _, files in os.walk(root):
8182
for f in files:
8283
if (f.endswith(".yaml") or f.endswith(".yml")) and f != schema_name:
83-
d = os.path.join(root, f)
84-
s = _find_schema(d, schema_name)
85-
if s:
86-
res.append(pool.apply_async(_validate, (s, d, parser, strict, False)))
84+
yaml_path = os.path.join(root, f)
85+
if should_exclude(yaml_path):
86+
continue
87+
schema_path = _find_schema(yaml_path, schema_name)
88+
if schema_path:
89+
res.append(pool.apply_async(_validate, (schema_path, yaml_path, parser, strict, False)))
8790
else:
88-
print("No schema found for: %s" % d)
91+
print(f"No schema found for: {yaml_path}")
8992

90-
print("Found %s yaml files." % len(res))
91-
print("Validating...")
93+
print(f"Found {len(res)} yaml files to validate...")
9294
for r in res:
9395
sub_results = r.get(timeout=300)
9496
error_messages.extend([str(sub_result) for sub_result in sub_results if not sub_result.isValid()])
@@ -98,16 +100,34 @@ def _validate_dir(root, schema_name, cpus, parser, strict):
98100
raise ValueError("\n----\n".join(set(error_messages)))
99101

100102

101-
def _router(paths, schema_name, cpus, parser, strict=True):
103+
def _router(paths, schema_name, cpus, parser, excludes=None, strict=True, verbose=False):
104+
EXCLUDE_REGEXES = tuple(re.compile(e) for e in excludes) if excludes else tuple()
105+
106+
def should_exclude(yaml_path):
107+
has_match = any(pattern.search(yaml_path) for pattern in EXCLUDE_REGEXES)
108+
if has_match and verbose:
109+
print("Skipping validation for %s due to exclude pattern" % yaml_path)
110+
return has_match
111+
102112
for path in paths:
103-
path = os.path.abspath(path)
104-
if os.path.isdir(path):
105-
_validate_dir(path, schema_name, cpus, parser, strict)
113+
abs_path = os.path.abspath(path)
114+
if os.path.exists(abs_path):
115+
print(f"Validating {path}...")
116+
else:
117+
raise ValueError(f"Path does not exist: {path}")
118+
119+
if os.path.isdir(abs_path):
120+
_validate_dir(abs_path, schema_name, cpus, parser, strict, should_exclude)
106121
else:
107-
_validate_single(path, schema_name, parser, strict)
122+
_validate_file(abs_path, schema_name, parser, strict, should_exclude)
108123

109124

110125
def main():
126+
def int_or_auto(num_cpu):
127+
if num_cpu == "auto":
128+
return multiprocessing.cpu_count()
129+
return int(num_cpu)
130+
111131
parser = argparse.ArgumentParser(description="Validate yaml files.")
112132
parser.add_argument(
113133
"paths",
@@ -116,21 +136,46 @@ def main():
116136
nargs="*",
117137
help="Paths to validate, either directories or files. Default is the current directory.",
118138
)
119-
parser.add_argument("-V", "--version", action="version", version=__version__)
120139
parser.add_argument("-s", "--schema", default="schema.yaml", help="filename of schema. Default is schema.yaml.")
121-
parser.add_argument("-n", "--cpu-num", default=4, type=int, help="number of CPUs to use. Default is 4.")
140+
parser.add_argument(
141+
"-e",
142+
"--exclude",
143+
metavar="PATTERN",
144+
action="append",
145+
help="Python regex used to exclude files from validation. Any substring match of a file's absolute path will be excluded. Uses default Python3 regex. Option can be supplied multiple times.",
146+
)
122147
parser.add_argument(
123148
"-p",
124149
"--parser",
125150
default="pyyaml",
126151
help='YAML library to load files. Choices are "ruamel" or "pyyaml" (default).',
127152
)
128153
parser.add_argument(
129-
"--no-strict", action="store_true", help="Disable strict mode, unexpected elements in the data will be accepted."
154+
"-n",
155+
"--cpu-num",
156+
default=4,
157+
type=int_or_auto,
158+
help="Number of child processes to spawn for validation. Default is 4. 'auto' to use CPU count.",
130159
)
160+
parser.add_argument(
161+
"-x",
162+
"--no-strict",
163+
action="store_true",
164+
help="Disable strict mode, unexpected elements in the data will be accepted.",
165+
)
166+
parser.add_argument("-v", "--verbose", action="store_true", help="show verbose information")
167+
parser.add_argument("-V", "--version", action="version", version=__version__)
131168
args = parser.parse_args()
132169
try:
133-
_router(args.paths, args.schema, args.cpu_num, args.parser, not args.no_strict)
170+
_router(
171+
paths=args.paths,
172+
schema_name=args.schema,
173+
cpus=args.cpu_num,
174+
parser=args.parser,
175+
excludes=args.exclude,
176+
strict=not args.no_strict,
177+
verbose=args.verbose,
178+
)
134179
except (SyntaxError, NameError, TypeError, ValueError) as e:
135180
print("Validation failed!\n%s" % str(e))
136181
exit(1)

yamale/tests/test_command_line.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,19 @@ def test_multiple_paths_bad_yaml():
7676
assert "map.bad: '12.5' is not a int." in e.value.message
7777

7878

79+
def test_excludes():
80+
command_line._router(
81+
paths=[
82+
"yamale/tests/command_line_fixtures/yamls/good.yaml",
83+
"yamale/tests/command_line_fixtures/yamls/bad.yaml",
84+
],
85+
schema_name="schema.yaml",
86+
excludes="bad.yaml",
87+
cpus=1,
88+
parser="PyYAML",
89+
)
90+
91+
7992
@pytest.mark.parametrize("parser", parsers)
8093
def test_good_relative_yaml(parser):
8194
command_line._router(
@@ -126,6 +139,17 @@ def test_bad_dir():
126139
command_line._router("yamale/tests/command_line_fixtures/yamls", "schema.yaml", 4, "PyYAML")
127140

128141

142+
def test_bad_path_raises():
143+
with pytest.raises(ValueError) as e:
144+
command_line._router(
145+
paths=["yamale/tests/command_line_fixtures/yamls/a path that does not exist.yaml"],
146+
schema_name="schema.yaml",
147+
cpus=1,
148+
parser="PyYAML",
149+
)
150+
assert "Path does not exist" in str(e)
151+
152+
129153
def test_bad_strict():
130154
with pytest.raises(ValueError) as e:
131155
command_line._router(

0 commit comments

Comments
 (0)