From 2b0564463c4147f343f87927260def90c2fbac49 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sun, 26 Apr 2026 18:54:01 +0200 Subject: [PATCH 1/3] Add support for schema overrides --- .github/workflows/changed-settings.yaml | 10 +- README.md | 2 + pyproject.toml | 124 +++++++++++++++++++++++- scripts/changed_settings.py | 66 ++++++++++--- 4 files changed, 182 insertions(+), 20 deletions(-) diff --git a/.github/workflows/changed-settings.yaml b/.github/workflows/changed-settings.yaml index b7e2e2b..2ebd019 100644 --- a/.github/workflows/changed-settings.yaml +++ b/.github/workflows/changed-settings.yaml @@ -16,8 +16,13 @@ on: required: false type: string default: '.contributes.configuration.properties' + schema_overrides_path: + description: A repo-relative path to a JSON file with optional "add", "remove", "transform" keys used to transform and override upstream schema. + required: false + type: string + default: 'sublime-package.overrides.json' version_file: - description: Relative path to the file that contains version information + description: Repo-relative path to the file that contains version information required: true type: string version_regexp: @@ -94,6 +99,7 @@ jobs: REPOSITORY_URL: ${{ inputs.repository_url }} CONFIGURATION_FILE_PATH: ${{ inputs.configuration_file_path }} CONFIGURATION_JQ_QUERY: ${{ inputs.configuration_jq_query }} + SCHEMA_OVERRIDES_PATH: ${{ inputs.schema_overrides_path }} run: | VERSION_FROM="${{ steps.tag-from.outputs.version }}" VERSION_TO="${{ steps.tag-to.outputs.version }}" @@ -104,7 +110,7 @@ jobs: { echo 'CHANGES<> "$GITHUB_OUTPUT" diff --git a/README.md b/README.md index 6442c26..a756eea 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ jobs: configuration_file_path: 'editors/code/package.json' # Optional configuration_jq_query: '.contributes.configuration.properties' + # Optional + schema_overrides_path: 'sublime-package.overrides.json' version_file: 'plugin.py' version_regexp: 'TAG = "([^"]+)"' # Optional string used to transform the tag captured by version_regexp. This can for example add a 'v' in front of the tag. The {} is replaced with the captured tag. diff --git a/pyproject.toml b/pyproject.toml index 6b9668f..2f8d96c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,127 @@ -[tool.ruff] -target-version = "py312" - [tool.pyright] pythonVersion = "3.12" typeCheckingMode = "strict" reportAny = "none" reportExplicitAny = "none" reportUnusedCallResult = "none" + +[tool.ruff] +line-length = 120 +target-version = "py312" + +[tool.ruff.format] +quote-style = "preserve" +indent-style = "space" +# Respect magic trailing commas. +skip-magic-trailing-comma = false +# Automatically detect the appropriate line ending. +line-ending = "auto" + +[tool.ruff.lint.isort] +case-sensitive = false +force-single-line = true +from-first = true +no-sections = true +order-by-type = false +required-imports = ["from __future__ import annotations"] + +[tool.ruff.lint] +# Enable preview rules. +preview = true +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +select = ["ALL"] +# Refer to https://docs.astral.sh/ruff/rules/ +ignore = [ + "ANN401", # https://docs.astral.sh/ruff/rules/any-type/ + "ARG002", # https://docs.astral.sh/ruff/rules/unused-method-argument/ + "ARG003", # https://docs.astral.sh/ruff/rules/unused-class-method-argument/ + "ARG005", # https://docs.astral.sh/ruff/rules/unused-lambda-argument/ + "B010", # https://docs.astral.sh/ruff/rules/set-attr-with-constant/ + "BLE", # https://docs.astral.sh/ruff/rules/blind-except/ + "C401", # https://docs.astral.sh/ruff/rules/unnecessary-generator-set/ + "C901", # https://docs.astral.sh/ruff/rules/complex-structure/ + "COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ + "CPY001", # https://docs.astral.sh/ruff/rules/missing-copyright-notice/ + "D100", # https://docs.astral.sh/ruff/rules/undocumented-public-module/#undocumented-public-module-d100 + "D101", # https://docs.astral.sh/ruff/rules/undocumented-public-class/ + "D102", # https://docs.astral.sh/ruff/rules/undocumented-public-method/ + "D103", # https://docs.astral.sh/ruff/rules/undocumented-public-function/ + "D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/ + "D105", # https://docs.astral.sh/ruff/rules/undocumented-magic-method/ + "D107", # https://docs.astral.sh/ruff/rules/undocumented-public-init/ + "D203", # https://docs.astral.sh/ruff/rules/incorrect-blank-line-before-class/ + "D205", # https://docs.astral.sh/ruff/rules/missing-blank-line-after-summary/ + "D212", # https://docs.astral.sh/ruff/rules/multi-line-summary-first-line/ + "D401", # https://docs.astral.sh/ruff/rules/non-imperative-mood/ + "D413", # https://docs.astral.sh/ruff/rules/missing-blank-line-after-last-section/ + "DOC201", # https://docs.astral.sh/ruff/rules/docstring-missing-returns/ + "DOC402", # https://docs.astral.sh/ruff/rules/docstring-missing-yields/ + "DOC501", # https://docs.astral.sh/ruff/rules/docstring-missing-exception/ + "DTZ005", # https://docs.astral.sh/ruff/rules/call-datetime-now-without-tzinfo/ + "EM", # https://docs.astral.sh/ruff/rules/#flake8-errmsg-em + "ERA001", # https://docs.astral.sh/ruff/rules/commented-out-code/ + "FBT001", # https://docs.astral.sh/ruff/rules/boolean-type-hint-positional-argument/ + "FBT002", # https://docs.astral.sh/ruff/rules/boolean-default-value-positional-argument/ + "FBT003", # https://docs.astral.sh/ruff/rules/boolean-positional-value-in-call/ + "FIX", # https://docs.astral.sh/ruff/rules/#flake8-fixme-fix + "N802", # https://docs.astral.sh/ruff/rules/invalid-function-name/#invalid-function-name-n802 + "PERF203", # https://docs.astral.sh/ruff/rules/try-except-in-loop/ + "PGH003", # https://docs.astral.sh/ruff/rules/blanket-type-ignore/ + "PGH004", # https://docs.astral.sh/ruff/rules/blanket-noqa/ + "PIE790", # https://docs.astral.sh/ruff/rules/unnecessary-placeholder/ + "PLC0415", # https://docs.astral.sh/ruff/rules/import-outside-top-level/ + "PLC2701", # https://docs.astral.sh/ruff/rules/import-private-name/ + "PLR0904", # https://docs.astral.sh/ruff/rules/too-many-public-methods/ + "PLR0911", # https://docs.astral.sh/ruff/rules/too-many-return-statements/ + "PLR0912", # https://docs.astral.sh/ruff/rules/too-many-branches/ + "PLR0913", # https://docs.astral.sh/ruff/rules/too-many-arguments/ + "PLR0914", # https://docs.astral.sh/ruff/rules/too-many-locals/ + "PLR0915", # https://docs.astral.sh/ruff/rules/too-many-statements/ + "PLR0917", # https://docs.astral.sh/ruff/rules/too-many-positional-arguments/ + "PLR1702", # https://docs.astral.sh/ruff/rules/too-many-nested-blocks/ + "PLR2004", # https://docs.astral.sh/ruff/rules/magic-value-comparison/ + "PLR6301", # https://docs.astral.sh/ruff/rules/no-self-use/ + "PLW0603", # https://docs.astral.sh/ruff/rules/global-statement/ + "PLW2901", # https://docs.astral.sh/ruff/rules/redefined-loop-name/ + "PT009", # https://docs.astral.sh/ruff/rules/pytest-unittest-assertion/ + "PT027", # https://docs.astral.sh/ruff/rules/pytest-unittest-raises-assertion/ + "PTH100", # https://docs.astral.sh/ruff/rules/os-path-abspath/ + "PTH109", # https://docs.astral.sh/ruff/rules/os-getcwd/ + "PTH110", # https://docs.astral.sh/ruff/rules/os-path-exists/ + "PTH111", # https://docs.astral.sh/ruff/rules/os-path-expanduser/ + "PTH112", # https://docs.astral.sh/ruff/rules/os-path-isdir/ + "PTH113", # https://docs.astral.sh/ruff/rules/os-path-isfile/ + "PTH118", # https://docs.astral.sh/ruff/rules/os-path-join/ + "PTH119", # https://docs.astral.sh/ruff/rules/os-path-basename/ + "PTH120", # https://docs.astral.sh/ruff/rules/os-path-dirname/ + "PTH121", # https://docs.astral.sh/ruff/rules/os-path-samefile/ + "PTH122", # https://docs.astral.sh/ruff/rules/os-path-splitext/ + "PTH208", # https://docs.astral.sh/ruff/rules/os-listdir/ + "Q", # flake8-quotes - https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt + "RUF001", # https://docs.astral.sh/ruff/rules/ambiguous-unicode-character-string/ + "RUF012", # https://docs.astral.sh/ruff/rules/mutable-class-default/ + "RUF023", # https://docs.astral.sh/ruff/rules/unsorted-dunder-slots/ + "RUF031", # https://docs.astral.sh/ruff/rules/incorrectly-parenthesized-tuple-in-subscript/ + "RUF067", # https://docs.astral.sh/ruff/rules/non-empty-init-module/ + "S101", # https://docs.astral.sh/ruff/rules/assert/ + "S110", # https://docs.astral.sh/ruff/rules/try-except-pass/ + "S307", # https://docs.astral.sh/ruff/rules/suspicious-eval-usage/ + "S310", # https://docs.astral.sh/ruff/rules/suspicious-url-open-usage/ + "S404", # https://docs.astral.sh/ruff/rules/suspicious-subprocess-import/ + "S603", # https://docs.astral.sh/ruff/rules/subprocess-without-shell-equals-true/ + "S606", # https://docs.astral.sh/ruff/rules/start-process-with-no-shell/ + "SIM102", # https://docs.astral.sh/ruff/rules/collapsible-if/ + "SIM103", # https://docs.astral.sh/ruff/rules/needless-bool/ + "SIM105", # https://docs.astral.sh/ruff/rules/suppressible-exception/ + "SIM110", # https://docs.astral.sh/ruff/rules/reimplemented-builtin/ + "SLF001", # https://docs.astral.sh/ruff/rules/private-member-access/ + "T20", # https://docs.astral.sh/ruff/rules/#flake8-print-t20 + "TD001", # https://docs.astral.sh/ruff/rules/invalid-todo-tag/ + "TD002", # https://docs.astral.sh/ruff/rules/missing-todo-author/ + "TD003", # https://docs.astral.sh/ruff/rules/missing-todo-link/ + "TID252", # https://docs.astral.sh/ruff/rules/relative-imports/ + "TRY002", # https://docs.astral.sh/ruff/rules/raise-vanilla-class/ + "TRY003", # https://docs.astral.sh/ruff/rules/raise-vanilla-args/ + "TRY301", # https://docs.astral.sh/ruff/rules/raise-within-try/ +] diff --git a/scripts/changed_settings.py b/scripts/changed_settings.py index 75bf1a2..6e1d27b 100755 --- a/scripts/changed_settings.py +++ b/scripts/changed_settings.py @@ -8,6 +8,7 @@ from pathlib import PurePosixPath from typing import Any from typing import cast +from typing import NotRequired from typing import TypedDict from urllib.error import HTTPError from urllib.request import urlopen @@ -32,15 +33,21 @@ class Configuration(TypedDict): type ConfigurationsDict = dict[str, Configuration] +class SchemaOverrides(TypedDict): + add: NotRequired[dict[str, Configuration]] + remove: NotRequired[list[str]] + transform: NotRequired[list[str]] + + def download_github_artifact_by_tag(repository_url: str, tag: str, target_dir: str) -> Path: archive_url = f'{repository_url}/archive/refs/tags/{tag}.zip' zip_path = Path(target_dir, f'archive-{re.sub(r'[<>:"/\\|?*]', '_', tag)}.zip') try: - with urlopen(archive_url) as response, zip_path.open('wb') as out_file: # noqa: S310 + with urlopen(archive_url) as response, zip_path.open('wb') as out_file: shutil.copyfileobj(response, out_file) - except HTTPError as ex: + except HTTPError: print(f'Error downloading {archive_url}', file=sys.stderr) - raise ex + raise return zip_path @@ -74,7 +81,6 @@ def get_parent_directory(zip_file: zipfile.ZipFile) -> str | None: Check if all files in the ZIP are contained within a parent directory. Returns str | None: Common parent name if present. """ - # Filter out directory entries and get top-level paths. top_levels: set[str] = set() for name in zip_file.namelist(): @@ -98,26 +104,40 @@ def generate_sublime_settings_markdown(settings: dict[str, Configuration]) -> st return f'```\n{sublime_settings_str}\n```' -def compare_json( - jq_query: str, contents_1: str, contents_2: str +def override_settings(settings: ConfigurationsDict, overrides: SchemaOverrides) -> ConfigurationsDict: + # Add (always at the beginning) + add = overrides.get('add', {}) + settings = {**add, **settings} + # Remove + remove = overrides.get('remove', []) + for key in list(settings.keys()): + if key in remove: + del settings[key] + # Transform + transform = overrides.get('transform', []) + for query in transform: + settings = jq(query, json.dumps(settings)) + return settings + + +def compare_settings( + settings_1: ConfigurationsDict, settings_2: ConfigurationsDict ) -> tuple[dict[str, Configuration], dict[str, Configuration], list[str]]: - flatten_settings_1 = jq(jq_query, contents_1) - flatten_settings_2 = jq(jq_query, contents_2) # Find added, removed and changed keys. added: dict[str, Configuration] = {} changed: dict[str, Configuration] = {} - removed: list[str] = [key for key in flatten_settings_1 if key not in flatten_settings_2] - for key, value in flatten_settings_2.items(): - if key not in flatten_settings_1: + removed: list[str] = [key for key in settings_1 if key not in settings_2] + for key, value in settings_2.items(): + if key not in settings_1: added[key] = value continue - if value != flatten_settings_1[key]: + if value != settings_1[key]: changed[key] = value return (added, changed, removed) def jq(query: str, contents: str) -> ConfigurationsDict: - return cast(ConfigurationsDict, + return cast('ConfigurationsDict', json.loads(subprocess.check_output(['jq', query], input=contents, text=True, encoding='utf-8'))) # noqa: S607 @@ -143,6 +163,9 @@ def main() -> None: help='A path to the configuration file relative to the repository_url.') parser.add_argument('configuration_jq_query', help='The JQ query to use to retrieve configuration settings.') + parser.add_argument('--schema-overrides-path', + default='sublime-package.overrides.json', + help='A file with augmentation used to transform the full schema.') parser.add_argument('tag_from', help='First tag to compare.') parser.add_argument('tag_to', help='Second tag to compare.') args = parser.parse_args() @@ -150,6 +173,7 @@ def main() -> None: repository_url: str = args.repository_url configuration_file_path: str = args.configuration_file_path configuration_jq_query: str = args.configuration_jq_query + schema_overrides_path = Path(args.schema_overrides_path) tag_from: str = args.tag_from tag_to: str = args.tag_to @@ -173,7 +197,13 @@ def main() -> None: ] if diff: - added, changed, removed = compare_json(configuration_jq_query, configuration_1, configuration_2) + settings_1 = jq(configuration_jq_query, configuration_1) + settings_2 = jq(configuration_jq_query, configuration_2) + if schema_overrides_path.is_file(): + overrides = json.loads(schema_overrides_path.read_text(encoding='utf-8')) + settings_1 = override_settings(settings_1, overrides) + settings_2 = override_settings(settings_2, overrides) + added, changed, removed = compare_settings(settings_1, settings_2) if added: output.append(markdown_collapsible_section( @@ -189,7 +219,13 @@ def main() -> None: key_list = '\n'.join([f' - `{k}`' for k in removed]) output.append(f'Removed keys (${len(key_list)}):\n{key_list}') - output.append(markdown_collapsible_section('All changes in schema', f'```diff\n{diff}\n```')) + output.extend(( + markdown_collapsible_section('All changes in the schema', f'```diff\n{diff}\n```'), + markdown_collapsible_section('Whole sublime-settings configuration', + generate_sublime_settings_markdown(settings_2)), + markdown_collapsible_section('Whole sublime-package schema', + f'```jsonc\n{json_serialize(settings_2)}\n```') + )) else: output.append('No changes') From a870d42dbfe40345bb9132079d94d2427a2d6034 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sun, 26 Apr 2026 18:57:48 +0200 Subject: [PATCH 2/3] add last --- scripts/changed_settings.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/scripts/changed_settings.py b/scripts/changed_settings.py index 6e1d27b..bc4bb65 100755 --- a/scripts/changed_settings.py +++ b/scripts/changed_settings.py @@ -105,9 +105,6 @@ def generate_sublime_settings_markdown(settings: dict[str, Configuration]) -> st def override_settings(settings: ConfigurationsDict, overrides: SchemaOverrides) -> ConfigurationsDict: - # Add (always at the beginning) - add = overrides.get('add', {}) - settings = {**add, **settings} # Remove remove = overrides.get('remove', []) for key in list(settings.keys()): @@ -117,7 +114,9 @@ def override_settings(settings: ConfigurationsDict, overrides: SchemaOverrides) transform = overrides.get('transform', []) for query in transform: settings = jq(query, json.dumps(settings)) - return settings + # Add (always at the beginning) + add = overrides.get('add', {}) + return {**add, **settings} def compare_settings( From 4d90ae4da48fbc4f0f1027d6eb1b1b28f5edc2f6 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sun, 26 Apr 2026 18:59:38 +0200 Subject: [PATCH 3/3] doc --- .github/workflows/changed-settings.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/changed-settings.yaml b/.github/workflows/changed-settings.yaml index 2ebd019..eb3f9c7 100644 --- a/.github/workflows/changed-settings.yaml +++ b/.github/workflows/changed-settings.yaml @@ -17,7 +17,7 @@ on: type: string default: '.contributes.configuration.properties' schema_overrides_path: - description: A repo-relative path to a JSON file with optional "add", "remove", "transform" keys used to transform and override upstream schema. + description: A repo-relative path to a JSON file with optional "add", "remove", "transform" keys used to transform and override upstream schema. "add" is a dictionary of schemas to add, "remove" a list of keys to remove and "transform" is a list of JQ queries to apply the input schema. required: false type: string default: 'sublime-package.overrides.json'