cfengine 0.7.0__tar.gz → 0.7.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {cfengine-0.7.0 → cfengine-0.7.2}/.github/workflows/pypi-publish.yml +1 -1
  2. {cfengine-0.7.0 → cfengine-0.7.2}/PKG-INFO +4 -4
  3. {cfengine-0.7.0 → cfengine-0.7.2}/pyproject.toml +3 -3
  4. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine.egg-info/PKG-INFO +4 -4
  5. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine.egg-info/SOURCES.txt +2 -2
  6. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine.egg-info/requires.txt +2 -2
  7. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine_cli/commands.py +18 -18
  8. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine_cli/docs.py +97 -57
  9. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine_cli/format.py +27 -27
  10. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine_cli/lint.py +32 -5
  11. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine_cli/main.py +2 -1
  12. cfengine-0.7.2/tests/shell/003-format.sh +37 -0
  13. {cfengine-0.7.0 → cfengine-0.7.2}/uv.lock +43 -115
  14. cfengine-0.7.0/src/cfengine_cli/markdowner.py +0 -251
  15. {cfengine-0.7.0 → cfengine-0.7.2}/.github/dependabot.yml +0 -0
  16. {cfengine-0.7.0 → cfengine-0.7.2}/.github/workflows/format.yml +0 -0
  17. {cfengine-0.7.0 → cfengine-0.7.2}/.github/workflows/lint.yml +0 -0
  18. {cfengine-0.7.0 → cfengine-0.7.2}/.github/workflows/test.yml +0 -0
  19. {cfengine-0.7.0 → cfengine-0.7.2}/.gitignore +0 -0
  20. {cfengine-0.7.0 → cfengine-0.7.2}/.python-version +0 -0
  21. {cfengine-0.7.0 → cfengine-0.7.2}/HACKING.md +0 -0
  22. {cfengine-0.7.0 → cfengine-0.7.2}/LICENSE +0 -0
  23. {cfengine-0.7.0 → cfengine-0.7.2}/README.md +0 -0
  24. {cfengine-0.7.0 → cfengine-0.7.2}/ci/01-install.sh +0 -0
  25. {cfengine-0.7.0 → cfengine-0.7.2}/ci/02-safe-tests.sh +0 -0
  26. {cfengine-0.7.0 → cfengine-0.7.2}/ci/03-unsafe-tests.sh +0 -0
  27. {cfengine-0.7.0 → cfengine-0.7.2}/setup.cfg +0 -0
  28. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine.egg-info/dependency_links.txt +0 -0
  29. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine.egg-info/entry_points.txt +0 -0
  30. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine.egg-info/top_level.txt +0 -0
  31. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine_cli/__init__.py +0 -0
  32. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine_cli/__main__.py +0 -0
  33. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine_cli/deptool.py +0 -0
  34. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine_cli/dev.py +0 -0
  35. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine_cli/paths.py +0 -0
  36. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine_cli/shell.py +0 -0
  37. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine_cli/utils.py +0 -0
  38. {cfengine-0.7.0 → cfengine-0.7.2}/src/cfengine_cli/version.py +0 -0
  39. {cfengine-0.7.0 → cfengine-0.7.2}/tests/__init__.py +0 -0
  40. {cfengine-0.7.0 → cfengine-0.7.2}/tests/format/001_hello_world.expected.cf +0 -0
  41. {cfengine-0.7.0 → cfengine-0.7.2}/tests/format/001_hello_world.input.cf +0 -0
  42. {cfengine-0.7.0 → cfengine-0.7.2}/tests/format/002_basics.expected.cf +0 -0
  43. {cfengine-0.7.0 → cfengine-0.7.2}/tests/format/002_basics.input.cf +0 -0
  44. {cfengine-0.7.0 → cfengine-0.7.2}/tests/format/003_wrapping.expected.cf +0 -0
  45. {cfengine-0.7.0 → cfengine-0.7.2}/tests/format/003_wrapping.input.cf +0 -0
  46. {cfengine-0.7.0 → cfengine-0.7.2}/tests/format/004_comments.expected.cf +0 -0
  47. {cfengine-0.7.0 → cfengine-0.7.2}/tests/format/004_comments.input.cf +0 -0
  48. {cfengine-0.7.0 → cfengine-0.7.2}/tests/run-format-tests.sh +0 -0
  49. {cfengine-0.7.0 → cfengine-0.7.2}/tests/run-shell-tests.sh +0 -0
  50. {cfengine-0.7.0 → cfengine-0.7.2}/tests/shell/001-help.sh +0 -0
  51. {cfengine-0.7.0 → cfengine-0.7.2}/tests/shell/002-version.sh +0 -0
  52. {cfengine-0.7.0 → cfengine-0.7.2}/tests/test_deps.py +0 -0
  53. {cfengine-0.7.0 → cfengine-0.7.2}/tests/test_paths.py +0 -0
  54. {cfengine-0.7.0 → cfengine-0.7.2}/tests/test_utils.py +0 -0
  55. {cfengine-0.7.0 → cfengine-0.7.2}/tests/test_version.py +0 -0
@@ -51,4 +51,4 @@ jobs:
51
51
  name: release-dists
52
52
  path: dist/
53
53
  - name: Publish release distributions to PyPI
54
- uses: pypa/gh-action-pypi-publish@v1.12.4
54
+ uses: pypa/gh-action-pypi-publish@v1.13.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cfengine
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: Human-oriented CLI for interacting with CFEngine tools
5
5
  License: GNU GENERAL PUBLIC LICENSE
6
6
  Version 3, 29 June 2007
@@ -692,12 +692,12 @@ Classifier: Programming Language :: Python :: 3.11
692
692
  Classifier: Programming Language :: Python :: 3.12
693
693
  Classifier: Programming Language :: Python :: 3.13
694
694
  Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
695
- Requires-Python: >=3.9
695
+ Requires-Python: >=3.10
696
696
  Description-Content-Type: text/markdown
697
697
  Requires-Dist: cf-remote>=0.6.4
698
698
  Requires-Dist: cfbs>=4.4.3
699
- Requires-Dist: tree-sitter-cfengine>=1.0.8
700
- Requires-Dist: tree-sitter>=0.23
699
+ Requires-Dist: tree-sitter-cfengine>=1.0.11
700
+ Requires-Dist: tree-sitter>=0.25
701
701
  Requires-Dist: markdown-it-py>=3.0.0
702
702
 
703
703
  # CFEngine command line interface (CLI)
@@ -8,12 +8,12 @@ dynamic = ["version"]
8
8
  description = "Human-oriented CLI for interacting with CFEngine tools"
9
9
  readme = "README.md"
10
10
  license = {file = "LICENSE"}
11
- requires-python = ">=3.9"
11
+ requires-python = ">=3.10"
12
12
  dependencies = [
13
13
  "cf-remote>=0.6.4",
14
14
  "cfbs>=4.4.3",
15
- "tree-sitter-cfengine>=1.0.8",
16
- "tree-sitter>=0.23",
15
+ "tree-sitter-cfengine>=1.0.11",
16
+ "tree-sitter>=0.25",
17
17
  "markdown-it-py>=3.0.0",
18
18
  ]
19
19
  classifiers = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cfengine
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: Human-oriented CLI for interacting with CFEngine tools
5
5
  License: GNU GENERAL PUBLIC LICENSE
6
6
  Version 3, 29 June 2007
@@ -692,12 +692,12 @@ Classifier: Programming Language :: Python :: 3.11
692
692
  Classifier: Programming Language :: Python :: 3.12
693
693
  Classifier: Programming Language :: Python :: 3.13
694
694
  Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
695
- Requires-Python: >=3.9
695
+ Requires-Python: >=3.10
696
696
  Description-Content-Type: text/markdown
697
697
  Requires-Dist: cf-remote>=0.6.4
698
698
  Requires-Dist: cfbs>=4.4.3
699
- Requires-Dist: tree-sitter-cfengine>=1.0.8
700
- Requires-Dist: tree-sitter>=0.23
699
+ Requires-Dist: tree-sitter-cfengine>=1.0.11
700
+ Requires-Dist: tree-sitter>=0.25
701
701
  Requires-Dist: markdown-it-py>=3.0.0
702
702
 
703
703
  # CFEngine command line interface (CLI)
@@ -28,7 +28,6 @@ src/cfengine_cli/docs.py
28
28
  src/cfengine_cli/format.py
29
29
  src/cfengine_cli/lint.py
30
30
  src/cfengine_cli/main.py
31
- src/cfengine_cli/markdowner.py
32
31
  src/cfengine_cli/paths.py
33
32
  src/cfengine_cli/shell.py
34
33
  src/cfengine_cli/utils.py
@@ -49,4 +48,5 @@ tests/format/003_wrapping.input.cf
49
48
  tests/format/004_comments.expected.cf
50
49
  tests/format/004_comments.input.cf
51
50
  tests/shell/001-help.sh
52
- tests/shell/002-version.sh
51
+ tests/shell/002-version.sh
52
+ tests/shell/003-format.sh
@@ -1,5 +1,5 @@
1
1
  cf-remote>=0.6.4
2
2
  cfbs>=4.4.3
3
- tree-sitter-cfengine>=1.0.8
4
- tree-sitter>=0.23
3
+ tree-sitter-cfengine>=1.0.11
4
+ tree-sitter>=0.25
5
5
  markdown-it-py>=3.0.0
@@ -47,46 +47,46 @@ def deploy() -> int:
47
47
  return r
48
48
 
49
49
 
50
- def _format_filename(filename):
50
+ def _format_filename(filename, line_length):
51
51
  if filename.startswith("./."):
52
52
  return
53
53
  if filename.endswith(".json"):
54
54
  format_json_file(filename)
55
55
  return
56
56
  if filename.endswith(".cf"):
57
- format_policy_file(filename)
57
+ format_policy_file(filename, line_length)
58
58
  return
59
59
  raise UserError(f"Unrecognized file format: {filename}")
60
60
 
61
61
 
62
- def _format_dirname(directory):
62
+ def _format_dirname(directory, line_length):
63
63
  for filename in find(directory, extension=".json"):
64
- _format_filename(filename)
64
+ _format_filename(filename, line_length)
65
65
  for filename in find(directory, extension=".cf"):
66
- _format_filename(filename)
66
+ _format_filename(filename, line_length)
67
67
 
68
68
 
69
- def format(args) -> int:
70
- if not args:
71
- _format_dirname(".")
69
+ def format(names, line_length) -> int:
70
+ if not names:
71
+ _format_dirname(".", line_length)
72
72
  return 0
73
- if len(args) == 1 and args[0] == "-":
73
+ if len(names) == 1 and names[0] == "-":
74
74
  # Special case, format policy file from stdin to stdout
75
- format_policy_fin_fout(sys.stdin, sys.stdout)
75
+ format_policy_fin_fout(sys.stdin, sys.stdout, line_length)
76
76
  return 0
77
77
 
78
- for arg in args:
79
- if arg == "-":
78
+ for name in names:
79
+ if name == "-":
80
80
  raise UserError(
81
81
  "The - argument has a special meaning and cannot be combined with other paths"
82
82
  )
83
- if not os.path.exists(arg):
84
- raise UserError(f"{arg} does not exist")
85
- if os.path.isfile(arg):
86
- _format_filename(arg)
83
+ if not os.path.exists(name):
84
+ raise UserError(f"{name} does not exist")
85
+ if os.path.isfile(name):
86
+ _format_filename(name, line_length)
87
87
  continue
88
- if os.path.isdir(arg):
89
- _format_dirname(arg)
88
+ if os.path.isdir(name):
89
+ _format_dirname(name, line_length)
90
90
  continue
91
91
  return 0
92
92
 
@@ -10,13 +10,12 @@ TODO: This code needs several adjustments to better fit into
10
10
 
11
11
  import os
12
12
  import json
13
- from shutil import which
14
13
  import subprocess
15
14
 
16
15
  import markdown_it
17
16
  from cfbs.pretty import pretty_file
18
17
 
19
- from cfengine_cli.markdowner import markdown_prettify
18
+ from cfengine_cli.lint import lint_policy_file
20
19
  from cfengine_cli.utils import UserError
21
20
 
22
21
 
@@ -110,7 +109,9 @@ def fn_extract(origin_path, snippet_path, _language, first_line, last_line):
110
109
  raise UserError(f"Couldn't open '{origin_path}' or '{snippet_path}'")
111
110
 
112
111
 
113
- def fn_check_syntax(origin_path, snippet_path, language, first_line, _last_line):
112
+ def fn_check_syntax(
113
+ origin_path, snippet_path, language, first_line, _last_line, snippet_number
114
+ ):
114
115
  snippet_abs_path = os.path.abspath(snippet_path)
115
116
 
116
117
  if not os.path.exists(snippet_path):
@@ -120,25 +121,11 @@ def fn_check_syntax(origin_path, snippet_path, language, first_line, _last_line)
120
121
 
121
122
  match language:
122
123
  case "cf":
123
- try:
124
- p = subprocess.run(
125
- ["/var/cfengine/bin/cf-promises", snippet_abs_path],
126
- capture_output=True,
127
- text=True,
128
- )
129
- err = p.stderr
130
-
131
- if err:
132
- err = err.replace(snippet_abs_path, f"{origin_path}:{first_line}")
133
- print(err)
134
- except OSError:
135
- raise UserError(f"'{snippet_abs_path}' doesn't exist")
136
- except ValueError:
137
- raise UserError("Invalid subprocess arguments")
138
- except subprocess.CalledProcessError:
139
- raise UserError(f"Couldn't run cf-promises on '{snippet_abs_path}'")
140
- except subprocess.TimeoutExpired:
141
- raise UserError("Timed out")
124
+ r = lint_policy_file(
125
+ snippet_abs_path, origin_path, first_line + 1, snippet_number
126
+ )
127
+ if r != 0:
128
+ raise UserError(f"Error when checking '{origin_path}'")
142
129
  case "json":
143
130
  try:
144
131
  with open(snippet_abs_path, "r") as f:
@@ -200,25 +187,35 @@ def fn_autoformat(_origin_path, snippet_path, language, _first_line, _last_line)
200
187
  raise UserError(f"Invalid json in '{snippet_path}'")
201
188
 
202
189
 
203
- def _markdown_code_checker(
204
- path, syntax_check, extract, replace, autoformat, languages, output_check, cleanup
205
- ):
206
- supported_languages = {"cf3": "cf", "json": "json", "yaml": "yml"}
190
+ def _translate_language(x):
191
+ if x == "cf3" or x == "cfengine3":
192
+ return "cf"
193
+ if x == "yaml":
194
+ return "yml"
195
+ return x
196
+
207
197
 
198
+ SUPPORTED_LANGUAGES = ["cf", "cfengine3", "cf3", "json", "yml", "yaml"]
199
+
200
+
201
+ def _process_markdown_code_blocks(
202
+ path, languages, extract, syntax_check, output_check, autoformat, replace, cleanup
203
+ ):
208
204
  if not os.path.exists(path):
209
205
  raise UserError("This path doesn't exist")
210
206
 
211
- if (
212
- syntax_check
213
- and "cf3" in languages
214
- and not which("/var/cfengine/bin/cf-promises")
215
- ):
216
- raise UserError("cf-promises is not installed")
217
-
207
+ languages = set(languages)
208
+ if "cf3" in languages or "cf" in languages or "cfengine3" in languages:
209
+ languages.add("cf3")
210
+ languages.add("cfengine3")
211
+ languages.add("cf")
212
+ if "yaml" in languages or "yml" in languages:
213
+ languages.add("yml")
214
+ languages.add("yaml")
218
215
  for language in languages:
219
- if language not in supported_languages:
216
+ if language not in SUPPORTED_LANGUAGES:
220
217
  raise UserError(
221
- f"Unsupported language '{language}'. The supported languages are: {", ".join(supported_languages.keys())}"
218
+ f"Unsupported language '{language}'. The supported languages are: {", ".join(SUPPORTED_LANGUAGES)}"
222
219
  )
223
220
 
224
221
  parsed_markdowns = get_markdown_files(path, languages)
@@ -234,8 +231,9 @@ def _markdown_code_checker(
234
231
  cb["first_line"] += offset
235
232
  cb["last_line"] += offset
236
233
 
237
- language = supported_languages[code_block["language"]]
238
- snippet_path = f"{origin_path}.snippet-{i + 1}.{language}"
234
+ language = _translate_language(code_block["language"])
235
+ snippet_number = i + 1
236
+ snippet_path = f"{origin_path}.snippet-{snippet_number}.{language}"
239
237
 
240
238
  flags = code_block["flags"]
241
239
  if "noextract" in flags or "skip" in flags:
@@ -258,12 +256,16 @@ def _markdown_code_checker(
258
256
  language,
259
257
  code_block["first_line"],
260
258
  code_block["last_line"],
259
+ snippet_number,
261
260
  )
262
261
  except Exception as e:
263
262
  if cleanup:
264
263
  os.remove(snippet_path)
265
264
  raise e
266
265
 
266
+ if output_check and "noexecute" not in code_block["flags"]:
267
+ fn_check_output()
268
+
267
269
  if autoformat and "noautoformat" not in code_block["flags"]:
268
270
  fn_autoformat(
269
271
  origin_path,
@@ -273,9 +275,6 @@ def _markdown_code_checker(
273
275
  code_block["last_line"],
274
276
  )
275
277
 
276
- if output_check and "noexecute" not in code_block["flags"]:
277
- fn_check_output()
278
-
279
278
  if replace and "noreplace" not in code_block["flags"]:
280
279
  offset = fn_replace(
281
280
  origin_path,
@@ -287,45 +286,86 @@ def _markdown_code_checker(
287
286
  )
288
287
  if cleanup:
289
288
  os.remove(snippet_path)
290
- if autoformat:
291
- markdown_prettify(origin_path)
289
+
290
+
291
+ def _run_black():
292
+ path = "."
293
+ assert os.path.isdir(path)
294
+ try:
295
+ subprocess.run(
296
+ ["black", path],
297
+ capture_output=True,
298
+ text=True,
299
+ check=True,
300
+ cwd=path,
301
+ )
302
+ except:
303
+ raise UserError(
304
+ "Encountered an error running black\nInstall: pipx install black"
305
+ )
306
+
307
+
308
+ def _run_prettier():
309
+ path = "."
310
+ assert os.path.isdir(path)
311
+ try:
312
+ subprocess.run(
313
+ ["prettier", "--write", "**.markdown", "**.md"],
314
+ capture_output=True,
315
+ text=True,
316
+ check=True,
317
+ cwd=path,
318
+ )
319
+ except:
320
+ raise UserError(
321
+ "Encountered an error running prettier\nInstall: npm install --global prettier"
322
+ )
292
323
 
293
324
 
294
325
  def update_docs() -> int:
295
326
  """
296
- Iterate through entire docs repo (.), autoformatting all JSONs.
297
-
298
- Will be expanded to more types of formatting in the future.
327
+ Iterate through entire docs repo (.), autoformatting as much as possible:
328
+ - python code with black
329
+ - markdown files with prettier
330
+ - code blocks inside markdown files are formatted for the formats supported by prettier
331
+ - JSON code blocks are re-formatted by cfbs pretty (we plan to expand this to CFEngine code blocks)
299
332
 
300
333
  Run by the command:
301
334
  cfengine dev docs-format
302
335
  """
303
- _markdown_code_checker(
336
+ print("Formatting python files with black...")
337
+ _run_black()
338
+ print("Formatting markdown files with prettier...")
339
+ _run_prettier()
340
+ print("Formatting markdown code blocks according to our rules...")
341
+ _process_markdown_code_blocks(
304
342
  path=".",
305
- syntax_check=False,
343
+ languages=["json"], # TODO: Add cfengine3 here
306
344
  extract=True,
307
- replace=True,
308
- autoformat=True,
309
- languages=["json"],
345
+ syntax_check=False,
310
346
  output_check=False,
347
+ autoformat=True,
348
+ replace=True,
311
349
  cleanup=True,
312
350
  )
313
351
  return 0
314
352
 
315
353
 
316
354
  def check_docs() -> int:
317
- """Entry point to be called by other files
355
+ """
356
+ Run checks / tests on docs.
357
+ Currently only JSON syntax checking.
318
358
 
319
359
  Run by the command:
320
- cfengine dev docs-checking"""
321
- _markdown_code_checker(
360
+ cfengine dev docs-check"""
361
+ _process_markdown_code_blocks(
322
362
  path=".",
323
- syntax_check=True,
363
+ languages=["json", "cf3"],
324
364
  extract=True,
325
- replace=False,
326
- autoformat=False,
327
- languages=["json"],
365
+ syntax_check=True,
328
366
  output_check=False,
367
+ autoformat=False,
368
+ replace=False,
329
369
  cleanup=True,
330
370
  )
331
371
  return 0
@@ -80,14 +80,14 @@ def split_generic_value(node, indent):
80
80
  return [stringify_single_line(node)]
81
81
 
82
82
 
83
- def split_generic_list(middle, indent):
83
+ def split_generic_list(middle, indent, line_length):
84
84
  elements = []
85
85
  for element in middle:
86
86
  if elements and element.type == ",":
87
87
  elements[-1] = elements[-1] + ","
88
88
  continue
89
89
  line = " " * indent + stringify_single_line(element)
90
- if len(line) < 80:
90
+ if len(line) < line_length:
91
91
  elements.append(line)
92
92
  else:
93
93
  lines = split_generic_value(element, indent)
@@ -96,50 +96,50 @@ def split_generic_list(middle, indent):
96
96
  return elements
97
97
 
98
98
 
99
- def maybe_split_generic_list(nodes, indent):
99
+ def maybe_split_generic_list(nodes, indent, line_length):
100
100
  string = " " * indent + stringify_children(nodes)
101
- if len(string) < 80:
101
+ if len(string) < line_length:
102
102
  return [string]
103
- return split_generic_list(nodes, indent)
103
+ return split_generic_list(nodes, indent, line_length)
104
104
 
105
105
 
106
- def split_rval_list(node, indent):
106
+ def split_rval_list(node, indent, line_length):
107
107
  assert node.type == "list"
108
108
  assert node.children[0].type == "{"
109
109
  first = text(node.children[0])
110
110
  last = " " * indent + text(node.children[-1])
111
111
  middle = node.children[1:-1]
112
- elements = maybe_split_generic_list(middle, indent + 2)
112
+ elements = maybe_split_generic_list(middle, indent + 2, line_length)
113
113
  return [first, *elements, last]
114
114
 
115
115
 
116
- def split_rval_call(node, indent):
116
+ def split_rval_call(node, indent, line_length):
117
117
  assert node.type == "call"
118
118
  assert node.children[0].type == "calling_identifier"
119
119
  assert node.children[1].type == "("
120
120
  first = text(node.children[0]) + "("
121
121
  last = " " * indent + text(node.children[-1])
122
122
  middle = node.children[2:-1]
123
- elements = maybe_split_generic_list(middle, indent + 2)
123
+ elements = maybe_split_generic_list(middle, indent + 2, line_length)
124
124
  return [first, *elements, last]
125
125
 
126
126
 
127
- def split_rval(node, indent):
127
+ def split_rval(node, indent, line_length):
128
128
  if node.type == "list":
129
- return split_rval_list(node, indent)
129
+ return split_rval_list(node, indent, line_length)
130
130
  if node.type == "call":
131
- return split_rval_call(node, indent)
131
+ return split_rval_call(node, indent, line_length)
132
132
  return [stringify_single_line(node)]
133
133
 
134
134
 
135
- def maybe_split_rval(node, indent, offset):
135
+ def maybe_split_rval(node, indent, offset, line_length):
136
136
  line = stringify_single_line(node)
137
- if len(line) + offset < 80:
137
+ if len(line) + offset < line_length:
138
138
  return [line]
139
- return split_rval(node, indent)
139
+ return split_rval(node, indent, line_length)
140
140
 
141
141
 
142
- def attempt_split_attribute(node, indent):
142
+ def attempt_split_attribute(node, indent, line_length):
143
143
  assert len(node.children) == 3
144
144
  lval = node.children[0]
145
145
  arrow = node.children[1]
@@ -148,22 +148,22 @@ def attempt_split_attribute(node, indent):
148
148
  if rval.type == "list" or rval.type == "call":
149
149
  prefix = " " * indent + text(lval) + " " + text(arrow) + " "
150
150
  offset = len(prefix)
151
- lines = maybe_split_rval(rval, indent, offset)
151
+ lines = maybe_split_rval(rval, indent, offset, line_length)
152
152
  lines[0] = prefix + lines[0]
153
153
  return lines
154
154
  return [stringify_single_line(node)]
155
155
 
156
156
 
157
- def stringify(node, indent):
157
+ def stringify(node, indent, line_length):
158
158
  single_line = " " * indent + stringify_single_line(node)
159
- if len(single_line) < 80:
159
+ if len(single_line) < line_length:
160
160
  return [single_line]
161
161
  if node.type == "attribute":
162
- return attempt_split_attribute(node, indent)
162
+ return attempt_split_attribute(node, indent, line_length)
163
163
  return [single_line]
164
164
 
165
165
 
166
- def autoformat(node, fmt, macro_indent, indent=0):
166
+ def autoformat(node, fmt, line_length, macro_indent, indent=0):
167
167
  previous = fmt.update_previous(node)
168
168
  if previous and previous.type == "macro" and text(previous).startswith("@else"):
169
169
  indent = macro_indent
@@ -190,12 +190,12 @@ def autoformat(node, fmt, macro_indent, indent=0):
190
190
  ]:
191
191
  indent += 2
192
192
  if node.type == "attribute":
193
- lines = stringify(node, indent)
193
+ lines = stringify(node, indent, line_length)
194
194
  fmt.print_lines(lines, indent=0)
195
195
  return
196
196
  if children:
197
197
  for child in children:
198
- autoformat(child, fmt, macro_indent, indent)
198
+ autoformat(child, fmt, line_length, macro_indent, indent)
199
199
  return
200
200
  if node.type in [",", ";"]:
201
201
  fmt.print_same_line(node)
@@ -203,7 +203,7 @@ def autoformat(node, fmt, macro_indent, indent=0):
203
203
  fmt.print(node, indent)
204
204
 
205
205
 
206
- def format_policy_file(filename):
206
+ def format_policy_file(filename, line_length):
207
207
  assert filename.endswith(".cf")
208
208
  PY_LANGUAGE = Language(tscfengine.language())
209
209
  parser = Parser(PY_LANGUAGE)
@@ -216,7 +216,7 @@ def format_policy_file(filename):
216
216
 
217
217
  root_node = tree.root_node
218
218
  assert root_node.type == "source_file"
219
- autoformat(root_node, fmt, macro_indent)
219
+ autoformat(root_node, fmt, line_length, macro_indent)
220
220
 
221
221
  new_data = fmt.buffer + "\n"
222
222
  if new_data != original_data.decode("utf-8"):
@@ -225,7 +225,7 @@ def format_policy_file(filename):
225
225
  print(f"Policy file '{filename}' was reformatted")
226
226
 
227
227
 
228
- def format_policy_fin_fout(fin, fout):
228
+ def format_policy_fin_fout(fin, fout, line_length):
229
229
  PY_LANGUAGE = Language(tscfengine.language())
230
230
  parser = Parser(PY_LANGUAGE)
231
231
 
@@ -236,7 +236,7 @@ def format_policy_fin_fout(fin, fout):
236
236
 
237
237
  root_node = tree.root_node
238
238
  assert root_node.type == "source_file"
239
- autoformat(root_node, fmt, macro_indent)
239
+ autoformat(root_node, fmt, line_length, macro_indent)
240
240
 
241
241
  new_data = fmt.buffer + "\n"
242
242
  fout.write(new_data)
@@ -97,9 +97,22 @@ def _walk(filename, lines, node) -> int:
97
97
  return errors
98
98
 
99
99
 
100
- def lint_policy_file(filename):
100
+ def lint_policy_file(
101
+ filename, original_filename=None, original_line=None, snippet=None
102
+ ):
103
+ assert original_filename is None or type(original_filename) is str
104
+ assert original_line is None or type(original_line) is int
105
+ assert snippet is None or type(snippet) is int
106
+ if (
107
+ original_filename is not None
108
+ or original_line is not None
109
+ or snippet is not None
110
+ ):
111
+ assert original_filename and os.path.isfile(original_filename)
112
+ assert original_line and original_line > 0
113
+ assert snippet and snippet > 0
101
114
  assert os.path.isfile(filename)
102
- assert filename.endswith(".cf")
115
+ assert filename.endswith((".cf", ".cfengine3", ".cf3", ".cf.sub"))
103
116
  PY_LANGUAGE = Language(tscfengine.language())
104
117
  parser = Parser(PY_LANGUAGE)
105
118
 
@@ -112,12 +125,26 @@ def lint_policy_file(filename):
112
125
  assert root_node.type == "source_file"
113
126
  errors = 0
114
127
  if not root_node.children:
115
- print(f"Error: Empty policy file '{filename}'")
128
+ if snippet:
129
+ assert original_filename and original_line
130
+ print(
131
+ f"Error: Empty policy snippet {snippet} at '{original_filename}:{original_line}'"
132
+ )
133
+ else:
134
+ print(f"Error: Empty policy file '{filename}'")
116
135
  errors += 1
117
136
  errors += _walk(filename, lines, root_node)
118
137
  if errors == 0:
119
- print(f"PASS: {filename}")
138
+ if snippet:
139
+ assert original_filename and original_line
140
+ print(f"PASS: Snippet {snippet} at '{original_filename}:{original_line}'")
141
+ else:
142
+ print(f"PASS: {filename}")
120
143
  return 0
121
144
 
122
- print(f"FAIL: {filename} ({errors} error{'s' if errors > 0 else ''})")
145
+ if snippet:
146
+ assert original_filename and original_line
147
+ print(f"FAIL: Snippet {snippet} at '{original_filename}:{original_line}'")
148
+ else:
149
+ print(f"FAIL: {filename} ({errors} error{'s' if errors > 0 else ''})")
123
150
  return errors
@@ -43,6 +43,7 @@ def _get_arg_parser():
43
43
  subp.add_parser("deploy", help="Deploy a built policy set")
44
44
  fmt = subp.add_parser("format", help="Autoformat .json and .cf files")
45
45
  fmt.add_argument("files", nargs="*", help="Files to format")
46
+ fmt.add_argument("--line-length", default=80, type=int, help="Maximum line length")
46
47
  subp.add_parser(
47
48
  "lint",
48
49
  help="Look for syntax errors and other simple mistakes",
@@ -86,7 +87,7 @@ def run_command_with_args(args) -> int:
86
87
  if args.command == "deploy":
87
88
  return commands.deploy()
88
89
  if args.command == "format":
89
- return commands.format(args.files)
90
+ return commands.format(args.files, args.line_length)
90
91
  if args.command == "lint":
91
92
  return commands.lint()
92
93
  if args.command == "report":