cfengine 0.17.0__tar.gz → 0.18.0__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 (162) hide show
  1. {cfengine-0.17.0 → cfengine-0.18.0}/.github/workflows/update-syntax-description.yml +2 -1
  2. {cfengine-0.17.0 → cfengine-0.18.0}/PKG-INFO +1 -1
  3. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine.egg-info/PKG-INFO +1 -1
  4. cfengine-0.18.0/src/cfengine.egg-info/scm_version.json +8 -0
  5. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/commands.py +5 -5
  6. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/docs.py +21 -9
  7. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/format.py +59 -30
  8. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/lint.py +11 -5
  9. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/main.py +10 -1
  10. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/syntax-description.json +1 -1
  11. cfengine-0.17.0/src/cfengine.egg-info/scm_version.json +0 -8
  12. {cfengine-0.17.0 → cfengine-0.18.0}/.github/dependabot.yml +0 -0
  13. {cfengine-0.17.0 → cfengine-0.18.0}/.github/workflows/lint-policy-in-other-repos.yml +0 -0
  14. {cfengine-0.17.0 → cfengine-0.18.0}/.github/workflows/make-check.yml +0 -0
  15. {cfengine-0.17.0 → cfengine-0.18.0}/.github/workflows/pypi-publish.yml +0 -0
  16. {cfengine-0.17.0 → cfengine-0.18.0}/.github/workflows/stale.yml +0 -0
  17. {cfengine-0.17.0 → cfengine-0.18.0}/.gitignore +0 -0
  18. {cfengine-0.17.0 → cfengine-0.18.0}/.python-version +0 -0
  19. {cfengine-0.17.0 → cfengine-0.18.0}/CLAUDE.md +0 -0
  20. {cfengine-0.17.0 → cfengine-0.18.0}/HACKING.md +0 -0
  21. {cfengine-0.17.0 → cfengine-0.18.0}/LICENSE +0 -0
  22. {cfengine-0.17.0 → cfengine-0.18.0}/Makefile +0 -0
  23. {cfengine-0.17.0 → cfengine-0.18.0}/README.md +0 -0
  24. {cfengine-0.17.0 → cfengine-0.18.0}/ci/01-install.sh +0 -0
  25. {cfengine-0.17.0 → cfengine-0.18.0}/ci/02-safe-tests.sh +0 -0
  26. {cfengine-0.17.0 → cfengine-0.18.0}/ci/03-unsafe-tests.sh +0 -0
  27. {cfengine-0.17.0 → cfengine-0.18.0}/pyproject.toml +0 -0
  28. {cfengine-0.17.0 → cfengine-0.18.0}/setup.cfg +0 -0
  29. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine.egg-info/SOURCES.txt +0 -0
  30. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine.egg-info/dependency_links.txt +0 -0
  31. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine.egg-info/entry_points.txt +0 -0
  32. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine.egg-info/requires.txt +0 -0
  33. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine.egg-info/scm_file_list.json +0 -0
  34. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine.egg-info/top_level.txt +0 -0
  35. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/__init__.py +0 -0
  36. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/__main__.py +0 -0
  37. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/changelog.py +0 -0
  38. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/deptool-README.md +0 -0
  39. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/deptool.py +0 -0
  40. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/dev.py +0 -0
  41. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/lint_csv.py +0 -0
  42. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/lint_yml.py +0 -0
  43. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/masterfiles/__init__.py +0 -0
  44. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/masterfiles/analyze.py +0 -0
  45. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/masterfiles/check_download_matches_git.py +0 -0
  46. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/masterfiles/download.py +0 -0
  47. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/masterfiles/generate_git_tags.py +0 -0
  48. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/masterfiles/generate_release_information.py +0 -0
  49. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/masterfiles/generate_vcf_download.py +0 -0
  50. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/masterfiles/generate_vcf_git_checkout.py +0 -0
  51. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/paths.py +0 -0
  52. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/profile.py +0 -0
  53. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/shell.py +0 -0
  54. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/syntax_tree.py +0 -0
  55. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/up.py +0 -0
  56. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/utils.py +0 -0
  57. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/validate.py +0 -0
  58. {cfengine-0.17.0 → cfengine-0.18.0}/src/cfengine_cli/version.py +0 -0
  59. {cfengine-0.17.0 → cfengine-0.18.0}/tests/README.md +0 -0
  60. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/001_hello_world.expected.cf +0 -0
  61. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/001_hello_world.input.cf +0 -0
  62. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/002_basics.expected.cf +0 -0
  63. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/002_basics.input.cf +0 -0
  64. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/003_wrapping.expected.cf +0 -0
  65. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/003_wrapping.input.cf +0 -0
  66. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/004_comments.expected.cf +0 -0
  67. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/004_comments.input.cf +0 -0
  68. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/005_bundle_comments.expected.cf +0 -0
  69. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/005_bundle_comments.input.cf +0 -0
  70. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/006_remove_empty_comments.expected.cf +0 -0
  71. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/006_remove_empty_comments.input.cf +0 -0
  72. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/007_class_guarded_empty_lines.expected.cf +0 -0
  73. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/007_class_guarded_empty_lines.input.cf +0 -0
  74. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/008_long_string.expected.cf +0 -0
  75. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/008_long_string.input.cf +0 -0
  76. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/009_single_line.expected.cf +0 -0
  77. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/009_single_line.input.cf +0 -0
  78. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/010_stakeholder.expected.cf +0 -0
  79. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/010_stakeholder.input.cf +0 -0
  80. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/011_macros.expected.cf +0 -0
  81. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/011_macros.input.cf +0 -0
  82. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/011_promises.expected.cf +0 -0
  83. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/011_promises.input.cf +0 -0
  84. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/012_sameline_comments.expected.cf +0 -0
  85. {cfengine-0.17.0 → cfengine-0.18.0}/tests/format/012_sameline_comments.input.cf +0 -0
  86. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/001_hello_world.cf +0 -0
  87. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/002_deprecations.expected.txt +0 -0
  88. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/002_deprecations.x.cf +0 -0
  89. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/003_name_lowercase.cf +0 -0
  90. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/003_name_lowercase.expected.txt +0 -0
  91. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/003_name_lowercase.x.cf +0 -0
  92. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/004_block_type.cf +0 -0
  93. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/004_block_type.expected.txt +0 -0
  94. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/004_block_type.x.cf +0 -0
  95. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/005_syntax_error.cf +0 -0
  96. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/005_syntax_error.expected.txt +0 -0
  97. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/005_syntax_error.x.cf +0 -0
  98. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/006_empty_file.expected.txt +0 -0
  99. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/006_empty_file.x.cf +0 -0
  100. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/007_namespace.cf +0 -0
  101. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/007_namespace.expected.txt +0 -0
  102. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/007_namespace.x.cf +0 -0
  103. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/008_bundle_shadows_function.cf +0 -0
  104. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/008_bundle_shadows_function.expected.txt +0 -0
  105. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/008_bundle_shadows_function.x.cf +0 -0
  106. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/009_unknown_function_inside_vars.cf +0 -0
  107. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/009_unknown_function_inside_vars.expected.txt +0 -0
  108. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/009_unknown_function_inside_vars.x.cf +0 -0
  109. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/010_mutually_exclusive_types_vars.cf +0 -0
  110. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/010_mutually_exclusive_types_vars.expected.txt +0 -0
  111. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/010_mutually_exclusive_types_vars.x.cf +0 -0
  112. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/011_invalid_attributes.expected.txt +0 -0
  113. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/011_invalid_attributes.x.cf +0 -0
  114. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/012_function_call_arg_count.cf +0 -0
  115. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/012_function_call_arg_count.expected.txt +0 -0
  116. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/012_function_call_arg_count.x.cf +0 -0
  117. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/013_num_args_body.expected.txt +0 -0
  118. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/013_num_args_body.x.cf +0 -0
  119. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/013_num_args_bundle.expected.txt +0 -0
  120. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/013_num_args_bundle.x.cf +0 -0
  121. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/013_num_args_bundle_body.cf +0 -0
  122. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/014_variadic_func_arg_count.cf +0 -0
  123. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/014_variadic_func_arg_count.expected.txt +0 -0
  124. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/014_variadic_func_arg_count.x.cf +0 -0
  125. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/015_macro_multi_def_bundle.cf +0 -0
  126. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/015_macro_multi_def_bundle.expected.txt +0 -0
  127. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/015_macro_multi_def_bundle.x.cf +0 -0
  128. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/016_half_promises.cf +0 -0
  129. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/016_half_promises.expected.txt +0 -0
  130. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/016_half_promises.x.cf +0 -0
  131. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/017_implies_body.cf +0 -0
  132. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/017_implies_body.expected.txt +0 -0
  133. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/017_implies_body.x.cf +0 -0
  134. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/018_nested_calls.cf +0 -0
  135. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/018_nested_calls.expected.txt +0 -0
  136. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/018_nested_calls.x.cf +0 -0
  137. {cfengine-0.17.0 → cfengine-0.18.0}/tests/lint/019_bundle_name_expansion.cf +0 -0
  138. {cfengine-0.17.0 → cfengine-0.18.0}/tests/run-format-tests.sh +0 -0
  139. {cfengine-0.17.0 → cfengine-0.18.0}/tests/run-lint-tests.sh +0 -0
  140. {cfengine-0.17.0 → cfengine-0.18.0}/tests/run-shell-tests.sh +0 -0
  141. {cfengine-0.17.0 → cfengine-0.18.0}/tests/run-up-validate-tests.sh +0 -0
  142. {cfengine-0.17.0 → cfengine-0.18.0}/tests/shell/001-help.sh +0 -0
  143. {cfengine-0.17.0 → cfengine-0.18.0}/tests/shell/002-version.sh +0 -0
  144. {cfengine-0.17.0 → cfengine-0.18.0}/tests/shell/003-format.sh +0 -0
  145. {cfengine-0.17.0 → cfengine-0.18.0}/tests/shell/004-format-check.sh +0 -0
  146. {cfengine-0.17.0 → cfengine-0.18.0}/tests/shell/005-lint.sh +0 -0
  147. {cfengine-0.17.0 → cfengine-0.18.0}/tests/unit/__init__.py +0 -0
  148. {cfengine-0.17.0 → cfengine-0.18.0}/tests/unit/test_changelog.py +0 -0
  149. {cfengine-0.17.0 → cfengine-0.18.0}/tests/unit/test_deps.py +0 -0
  150. {cfengine-0.17.0 → cfengine-0.18.0}/tests/unit/test_format.py +0 -0
  151. {cfengine-0.17.0 → cfengine-0.18.0}/tests/unit/test_lint_csv.py +0 -0
  152. {cfengine-0.17.0 → cfengine-0.18.0}/tests/unit/test_lint_yml.py +0 -0
  153. {cfengine-0.17.0 → cfengine-0.18.0}/tests/unit/test_paths.py +0 -0
  154. {cfengine-0.17.0 → cfengine-0.18.0}/tests/unit/test_utils.py +0 -0
  155. {cfengine-0.17.0 → cfengine-0.18.0}/tests/unit/test_version.py +0 -0
  156. {cfengine-0.17.0 → cfengine-0.18.0}/tests/up-validate/001_complex_config.yaml +0 -0
  157. {cfengine-0.17.0 → cfengine-0.18.0}/tests/up-validate/002_version.x.yaml +0 -0
  158. {cfengine-0.17.0 → cfengine-0.18.0}/tests/up-validate/003_box.x.yaml +0 -0
  159. {cfengine-0.17.0 → cfengine-0.18.0}/tests/up-validate/004_count.x.yaml +0 -0
  160. {cfengine-0.17.0 → cfengine-0.18.0}/tests/up-validate/mocks/cloud_config.json +0 -0
  161. {cfengine-0.17.0 → cfengine-0.18.0}/tests/up-validate/mocks/vagrant +0 -0
  162. {cfengine-0.17.0 → cfengine-0.18.0}/uv.lock +0 -0
@@ -55,8 +55,9 @@ jobs:
55
55
  fi
56
56
  - name: Create Pull Request
57
57
  if: env.CHANGES_DETECTED == 'true'
58
- uses: cfengine/create-pull-request@v6
58
+ uses: cfengine/create-pull-request@v7
59
59
  with:
60
+ sign-commits: true
60
61
  title: Updated syntax-description.json
61
62
  body: Automated update to syntax-description.json [the `update-syntax-description` workflow](https://github.com/cfengine/cfengine-cli/tree/main/.github/workflows/update-syntax-description.yml).
62
63
  reviewers: |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cfengine
3
- Version: 0.17.0
3
+ Version: 0.18.0
4
4
  Summary: Human-oriented CLI for interacting with CFEngine policy language, projects, and other CFEngine tools
5
5
  License: GNU GENERAL PUBLIC LICENSE
6
6
  Version 3, 29 June 2007
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cfengine
3
- Version: 0.17.0
3
+ Version: 0.18.0
4
4
  Summary: Human-oriented CLI for interacting with CFEngine policy language, projects, and other CFEngine tools
5
5
  License: GNU GENERAL PUBLIC LICENSE
6
6
  Version 3, 29 June 2007
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.18.0",
3
+ "distance": 0,
4
+ "node": "g226b1b9a975f32e3626fd1bd8cc4578e2a6f2b94",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-07"
8
+ }
@@ -52,14 +52,14 @@ def format(names, line_length, check) -> int:
52
52
  return format_paths(names, line_length, check)
53
53
 
54
54
 
55
- def _lint(files, strict) -> int:
55
+ def _lint(files, strict, syntax_path) -> int:
56
56
  if not files:
57
- return lint_args(["."], strict)
58
- return lint_args(files, strict)
57
+ return lint_args(["."], strict, syntax_path)
58
+ return lint_args(files, strict, syntax_path)
59
59
 
60
60
 
61
- def lint(files, strict) -> int:
62
- errors = _lint(files, strict)
61
+ def lint(files, strict, syntax_path) -> int:
62
+ errors = _lint(files, strict, syntax_path)
63
63
  if errors == 0:
64
64
  print("Success, no errors found.")
65
65
  elif errors == 1:
@@ -61,15 +61,16 @@ def extract_inline_code(path, languages):
61
61
  indent = count_indent(lines[first_line])
62
62
  # Index of first line to NOT include, the line after closing triple backtick:
63
63
  last_line = child.map[1]
64
+ lines_to_extract = lines[first_line:last_line]
65
+ _remove_indentation(lines_to_extract, indent)
64
66
  yield {
65
67
  "language": language,
66
68
  "flags": flags,
67
69
  "first_line": child.map[0],
68
70
  "last_line": child.map[1],
69
71
  "indent": indent,
70
- "lines": lines[
71
- first_line:last_line
72
- ], # Includes backtick fences on both sides
72
+ # TODO: Dead code, the actual lines are extracted "twice" (2 places in the codebase)
73
+ "lines": lines_to_extract, # Includes backtick fences on both sides
73
74
  }
74
75
 
75
76
 
@@ -97,12 +98,24 @@ def get_markdown_files(start, languages):
97
98
  return return_dict
98
99
 
99
100
 
101
+ def _remove_indentation(snippet_lines, indent):
102
+ for i, line in enumerate(snippet_lines):
103
+ if line == "":
104
+ continue
105
+ assert line.startswith(" " * indent)
106
+ snippet_lines[i] = line[indent:]
107
+
108
+
100
109
  def fn_extract(origin_path, snippet_path, _language, first_line, last_line):
101
110
  try:
102
111
  with open(origin_path, "r") as f:
103
- content = f.read()
104
-
105
- code_snippet = "\n".join(content.split("\n")[first_line + 1 : last_line - 1])
112
+ lines = f.readlines()
113
+ lines = [x[0:-1] for x in lines] # Remove newlines
114
+ fence = lines[first_line]
115
+ indent = count_indent(fence)
116
+ snippet_lines = lines[first_line + 1 : last_line - 1]
117
+ _remove_indentation(snippet_lines, indent)
118
+ code_snippet = "\n".join(snippet_lines)
106
119
 
107
120
  with open(snippet_path, "w") as f:
108
121
  f.write(code_snippet + "\n")
@@ -174,7 +187,7 @@ def fn_replace(origin_path, snippet_path, _language, first_line, last_line, inde
174
187
  origin_lines = f.read().split("\n")
175
188
  pretty_lines = pretty_content.split("\n")
176
189
 
177
- pretty_lines = [" " * indent + x for x in pretty_lines]
190
+ pretty_lines = ["" if x == "" else " " * indent + x for x in pretty_lines]
178
191
 
179
192
  offset = len(pretty_lines) - len(
180
193
  origin_lines[first_line + 1 : last_line - 1]
@@ -213,7 +226,6 @@ def fn_autoformat(
213
226
  except json.decoder.JSONDecodeError:
214
227
  raise UserError(f"Invalid json in '{snippet_path}'")
215
228
  case "cf":
216
- # Note: Dead code - Not used for CFEngine policy yet
217
229
  format_policy_file(snippet_path, 80, False)
218
230
  return False
219
231
 
@@ -434,7 +446,7 @@ def _format_docs_single_arg(path):
434
446
  formatted = True
435
447
  _process_markdown_code_blocks(
436
448
  path=path,
437
- languages=["json"], # TODO: Add cfengine3 here
449
+ languages=["json", "cfengine3"],
438
450
  extract=True,
439
451
  syntax_check=False,
440
452
  output_check=False,
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from enum import IntEnum
3
4
  from typing import IO
4
5
 
5
6
  import json
@@ -38,6 +39,14 @@ PROMISER_PARTS = {"promiser", "->", "stakeholder"}
38
39
  SAMELINE_COMMENT_SEPARATOR = " "
39
40
 
40
41
 
42
+ class FormatResult(IntEnum):
43
+ """Outcome of a formatting operation."""
44
+
45
+ NO_CHANGE = 0 # File was already formatted, no reformat needed
46
+ NEEDS_FORMAT = 1 # Reformat needed (reported in check mode)
47
+ REFORMATTED = 2 # File was reformatted successfully
48
+
49
+
41
50
  def _has_direct_macro(node: Node) -> bool:
42
51
  """Check if any direct child of a node is a macro (non-recursive)."""
43
52
  return any(child.type == "macro" for child in node.children)
@@ -64,12 +73,8 @@ def _contains_list_with_comment(nodes: Node | list[Node]) -> bool:
64
73
  return _contains_list_with_comment(nodes.children)
65
74
 
66
75
 
67
- def format_json_file(filename: str, check: bool) -> int:
68
- """Reformat a JSON file in place using cfbs pretty-printer.
69
-
70
- Returns 0 in case of successful reformat or no reformat needed.
71
- Returns 1 when check is True and reformat is needed.
72
- """
76
+ def format_json_file(filename: str, check: bool) -> FormatResult:
77
+ """Reformat a JSON file in place using cfbs pretty-printer."""
73
78
  assert filename.endswith(".json")
74
79
 
75
80
  if check:
@@ -78,16 +83,17 @@ def format_json_file(filename: str, check: bool) -> int:
78
83
  assert type(success) is bool
79
84
  if not success:
80
85
  print(f"JSON file '{filename}' needs reformatting")
81
- return int(not success)
86
+ return FormatResult.NEEDS_FORMAT
87
+ return FormatResult.NO_CHANGE
82
88
 
83
89
  try:
84
90
  reformatted = pretty_file(filename)
85
91
  if reformatted:
86
92
  print(f"JSON file '{filename}' was reformatted")
87
- return 0 # Successfully reformatted or no reformat needed
93
+ return FormatResult.REFORMATTED
94
+ return FormatResult.NO_CHANGE
88
95
  except json.decoder.JSONDecodeError as e:
89
- print(f"JSON file '{filename}' invalid ({str(e)})")
90
- return 1
96
+ raise UserError(f"JSON file '{filename}' invalid ({str(e)})")
91
97
 
92
98
 
93
99
  def text(node: Node) -> str:
@@ -1008,11 +1014,9 @@ def _autoformat(
1008
1014
  # ---------------------------------------------------------------------------
1009
1015
 
1010
1016
 
1011
- def format_policy_file(filename: str, line_length: int, check: bool) -> int:
1017
+ def format_policy_file(filename: str, line_length: int, check: bool) -> FormatResult:
1012
1018
  """Format a .cf policy file in place, writing only if content changed.
1013
1019
 
1014
- Returns 0 in case of successful reformat or no reformat needed.
1015
- Returns 1 when check is True and reformat is needed.
1016
1020
  Raises PolicySyntaxError when the file has syntax errors."""
1017
1021
  assert filename.endswith(".cf")
1018
1022
 
@@ -1031,21 +1035,28 @@ def format_policy_file(filename: str, line_length: int, check: bool) -> int:
1031
1035
  fmt = Formatter()
1032
1036
  _autoformat(root_node, fmt, line_length)
1033
1037
 
1034
- new_data = fmt.buffer + "\n"
1038
+ new_data = fmt.buffer
1039
+ if not new_data.endswith("\n"):
1040
+ # TODO: Look into why formatter sometimes outputs
1041
+ # trailing newline and other times not.
1042
+ new_data += "\n"
1043
+ assert new_data.endswith("\n") and not new_data.endswith("\n\n")
1044
+
1035
1045
  if new_data != original_data.decode("utf-8"):
1036
1046
  if check:
1037
1047
  print(f"Policy file '{filename}' needs reformatting")
1038
- return 1
1048
+ return FormatResult.NEEDS_FORMAT
1039
1049
 
1040
1050
  with open(filename, "w") as f:
1041
1051
  f.write(new_data)
1042
1052
  print(f"Policy file '{filename}' was reformatted")
1043
- return 0
1053
+ return FormatResult.REFORMATTED
1054
+ return FormatResult.NO_CHANGE
1044
1055
 
1045
1056
 
1046
1057
  def format_policy_fin_fout(
1047
1058
  fin: IO[str], fout: IO[str], line_length: int, check: bool
1048
- ) -> int:
1059
+ ) -> FormatResult:
1049
1060
  """Format CFEngine policy read from fin, writing the result to fout.
1050
1061
 
1051
1062
  Raises PolicySyntaxError when the input has syntax errors."""
@@ -1065,10 +1076,12 @@ def format_policy_fin_fout(
1065
1076
 
1066
1077
  new_data = fmt.buffer + "\n"
1067
1078
  fout.write(new_data)
1068
- return 0
1079
+ if new_data != original_data.decode("utf-8"):
1080
+ return FormatResult.REFORMATTED
1081
+ return FormatResult.NO_CHANGE
1069
1082
 
1070
1083
 
1071
- def _format_filename(filename: str, line_length: int, check: bool) -> int:
1084
+ def _format_filename(filename: str, line_length: int, check: bool) -> FormatResult:
1072
1085
  """Format a single file.
1073
1086
 
1074
1087
  Raises PolicySyntaxError for .cf files with syntax errors."""
@@ -1079,8 +1092,17 @@ def _format_filename(filename: str, line_length: int, check: bool) -> int:
1079
1092
  raise UserError(f"Unrecognized file format: {filename}")
1080
1093
 
1081
1094
 
1082
- def _format_dirname(directory: str, line_length: int, check: bool) -> int:
1083
- ret = 0
1095
+ def _combine_results(a: FormatResult, b: FormatResult) -> FormatResult:
1096
+ """Combine two format results. A non-NO_CHANGE result takes precedence.
1097
+
1098
+ Within a single format run, `check` is fixed, so NEEDS_FORMAT and
1099
+ REFORMATTED never occur together — one always dominates NO_CHANGE."""
1100
+ return b if b != FormatResult.NO_CHANGE else a
1101
+
1102
+
1103
+ def _format_dirname(directory: str, line_length: int, check: bool) -> FormatResult:
1104
+ """Format files in directory recursively."""
1105
+ ret = FormatResult.NO_CHANGE
1084
1106
  for root, dirs, files in os.walk(directory):
1085
1107
  # Don't recurse into hidden folders
1086
1108
  dirs[:] = [d for d in dirs if not d.startswith(".")]
@@ -1100,18 +1122,20 @@ def _format_dirname(directory: str, line_length: int, check: bool) -> int:
1100
1122
  continue # Test files skipped during directory traversal
1101
1123
  filepath = os.path.join(root, name)
1102
1124
  if name.endswith(".json") or name.endswith(".cf"):
1103
- ret |= _format_filename(filepath, line_length, check)
1125
+ ret = _combine_results(
1126
+ ret, _format_filename(filepath, line_length, check)
1127
+ )
1104
1128
  return ret
1105
1129
 
1106
1130
 
1107
- def _format_paths_inner(names, line_length, check) -> int:
1131
+ def _format_paths_inner(names, line_length, check) -> FormatResult:
1108
1132
  if not names:
1109
1133
  return _format_dirname(".", line_length, check)
1110
1134
  if len(names) == 1 and names[0] == "-":
1111
1135
  # Special case, format policy file from stdin to stdout
1112
1136
  return format_policy_fin_fout(sys.stdin, sys.stdout, line_length, check)
1113
1137
 
1114
- ret = 0
1138
+ ret = FormatResult.NO_CHANGE
1115
1139
  for name in names:
1116
1140
  if name == "-":
1117
1141
  raise UserError(
@@ -1120,14 +1144,12 @@ def _format_paths_inner(names, line_length, check) -> int:
1120
1144
  if not os.path.exists(name):
1121
1145
  raise UserError(f"{name} does not exist")
1122
1146
  if os.path.isfile(name):
1123
- ret |= _format_filename(name, line_length, check)
1147
+ ret = _combine_results(ret, _format_filename(name, line_length, check))
1124
1148
  continue
1125
1149
  if os.path.isdir(name):
1126
- ret |= _format_dirname(name, line_length, check)
1150
+ ret = _combine_results(ret, _format_dirname(name, line_length, check))
1127
1151
  continue
1128
- if check:
1129
- return ret
1130
- return 0
1152
+ return ret
1131
1153
 
1132
1154
 
1133
1155
  def format_paths(names, line_length, check) -> int:
@@ -1138,7 +1160,14 @@ def format_paths(names, line_length, check) -> int:
1138
1160
  needed), 1 when reformatting is needed in check mode or a policy file
1139
1161
  has syntax errors."""
1140
1162
  try:
1141
- return _format_paths_inner(names, line_length, check)
1163
+ r = _format_paths_inner(names, line_length, check)
1164
+ if r == FormatResult.NEEDS_FORMAT:
1165
+ assert check
1166
+ # File needs reformatting
1167
+ return 1
1168
+ assert r in (FormatResult.NO_CHANGE, FormatResult.REFORMATTED)
1169
+ # Success - no format needed or successfully reformatted
1170
+ return 0
1142
1171
  except PolicySyntaxError as e:
1143
1172
  print(f"Error: {e}")
1144
1173
  return 1
@@ -81,9 +81,9 @@ class SyntaxData:
81
81
  BUILTIN_PROMISE_TYPES = {}
82
82
  BUILTIN_FUNCTIONS = {}
83
83
 
84
- def __init__(self):
84
+ def __init__(self, syntax_path=None):
85
85
  """Load the bundled syntax-description.json and derive lookup dicts."""
86
- self._data_dict = self._load_syntax_description()
86
+ self._data_dict = self._load_syntax_description(path=syntax_path)
87
87
  self._derive_syntax_dicts(self._data_dict)
88
88
 
89
89
  assert self.BUILTIN_BODY_TYPES
@@ -1100,6 +1100,7 @@ def _lint_main(
1100
1100
  state=None,
1101
1101
  snippet: Snippet | None = None,
1102
1102
  syntax_data=None,
1103
+ syntax_path: str | None = None,
1103
1104
  ) -> int:
1104
1105
  """This is the main function used for linting, it does all the steps on all
1105
1106
  the arguments (files / folders).
@@ -1127,7 +1128,10 @@ def _lint_main(
1127
1128
  state.mode = Mode.SYNTAX
1128
1129
 
1129
1130
  if syntax_data is None:
1130
- syntax_data = SyntaxData()
1131
+ try:
1132
+ syntax_data = SyntaxData(syntax_path)
1133
+ except FileNotFoundError:
1134
+ raise UserError(f"'{syntax_path}' does not exist")
1131
1135
 
1132
1136
  filenames = _args_to_filenames(args)
1133
1137
 
@@ -1379,9 +1383,11 @@ def lint_single_file(file: str, strict: bool = True) -> int:
1379
1383
  return _lint_main([file], strict)
1380
1384
 
1381
1385
 
1382
- def lint_args(args: Iterable[str], strict: bool = True) -> int:
1386
+ def lint_args(
1387
+ args: Iterable[str], strict: bool = True, syntax_path: str | None = None
1388
+ ) -> int:
1383
1389
  """Lint a list of args (files / folders)"""
1384
- return _lint_main(list(args), strict)
1390
+ return _lint_main(list(args), strict, syntax_path=syntax_path)
1385
1391
 
1386
1392
 
1387
1393
  def lint_policy_file_snippet(
@@ -59,6 +59,11 @@ def _get_arg_parser():
59
59
  default="yes",
60
60
  help="Strict mode. Default=yes, checks for undefined promise types, bundles, bodies, functions",
61
61
  )
62
+ lnt.add_argument(
63
+ "--syntax-description",
64
+ type=str,
65
+ help="Lint based on a user given syntax description",
66
+ )
62
67
  lnt.add_argument("files", nargs="*", help="Files to lint")
63
68
  subp.add_parser(
64
69
  "report",
@@ -188,7 +193,11 @@ def run_command_with_args(args) -> int:
188
193
  if args.command == "format":
189
194
  return commands.format(args.files, args.line_length, args.check)
190
195
  if args.command == "lint":
191
- return commands.lint(args.files, (args.strict.lower() in ("y", "ye", "yes")))
196
+ return commands.lint(
197
+ args.files,
198
+ (args.strict.lower() in ("y", "ye", "yes")),
199
+ args.syntax_description,
200
+ )
192
201
  if args.command == "report":
193
202
  return commands.report()
194
203
  if args.command == "run":
@@ -9409,7 +9409,7 @@
9409
9409
  },
9410
9410
  "signals": {
9411
9411
  "attribute": "signals",
9412
- "range": "(hup|int|trap|kill|pipe|cont|abrt|stop|quit|term|child|usr1|usr2|bus|segv|[0-9]+s?)",
9412
+ "range": "((?i:hup|int|trap|kill|pipe|cont|abrt|stop|quit|term|child|usr1|usr2|bus|segv)|[0-9]+s?)",
9413
9413
  "status": "normal",
9414
9414
  "type": "slist",
9415
9415
  "visibility": "promiseType"
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.17.0",
3
- "distance": 0,
4
- "node": "g052defd5991547f1ddcc7c1eb64f5ca22682be34",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-07-01"
8
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes