Skip to content

Commit

Permalink
Sync vscode settings from devcontainer (#65)
Browse files Browse the repository at this point in the history
Co-authored-by: Mateusz Krakowiak <[email protected]>
Co-authored-by: Mateusz Krakowiak <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
4 people authored Feb 11, 2025
1 parent d162ae5 commit 5d6755e
Show file tree
Hide file tree
Showing 9 changed files with 546 additions and 11 deletions.
15 changes: 15 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ repos:
files: (README.md|.pre-commit-hooks.yaml|dev_tools/generate_hook_docs.py)
additional_dependencies:
- pre-commit>=3.5.0
- id: sync-vscode-config
name: Sync VSCode Config
description: Sync settings and extensions from devcontainer.json to json files in .vscode folder
entry: dev_tools/sync_vscode_config.py
language: python
args: [--verbose]
files: |
(?x)^(
(.devcontainer/)?devcontainer.json|
.vscode/extensions.json|
.vscode/settings.json
)
pass_filenames: false
additional_dependencies:
- pyjson5>=1.6.8
- repo: https://github.com/pre-commit/pre-commit
rev: v3.5.0 # Last version to support Python 3.8
hooks:
Expand Down
21 changes: 21 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,27 @@
entry: check-useless-exclude-paths-hooks
language: python
additional_dependencies: ["pre-commit >= 3.5.0"]
- id: sync-vscode-config
name: Sync VSCode Config
description: |-
Sync VSCode settings and extensions from `devcontainer.json` to `.vscode` folder.
`devcontainer.json` will be now your source of truth.
Entries defined in `settings.json` and `extensions.json` which don't exist in `devcontainer.json` will be left as is.
If `settings.json` and `extensions.json` are ignored in Git, consider running the hook in `post-checkout` and `post-merge` stages by overwriting the `stages` config.
In this case, define your `default_install_hook_types` in the pre-commit config and set `always_run: true` for this hook.
entry: sync-vscode-config
language: python
stages:
- pre-commit
files: |
(?x)^(
(.devcontainer/)?devcontainer.json|
.vscode/extensions.json|
.vscode/settings.json
)
pass_filenames: false
additional_dependencies: [pyjson5>=1.6.8]
- id: check-ownership
name: Check `.github/CODEOWNERS` consistency (can take a while)
description: |
Expand Down
7 changes: 1 addition & 6 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"charliermarsh.ruff",
"eamodio.gitlens",
Expand All @@ -10,7 +7,5 @@
"ms-python.vscode-pylance",
"ms-vscode-remote.vscode-remote-extensionpack",
"yzhang.markdown-all-in-one"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": []
]
}
9 changes: 7 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
"editor.formatOnType": true
},
"files.watcherExclude": {
"**/__pycache__/**": true,
}
"**/__pycache__/**": true
},
"python.testing.pytestArgs": [
"dev_tools"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ These tools are used to help developers in their day-to-day tasks.
- [`check-shellscript-set-options`](#check-shellscript-set-options)
- [`check-jira-reference-in-todo`](#check-jira-reference-in-todo)
- [`check-non-existing-and-duplicate-excludes`](#check-non-existing-and-duplicate-excludes)
- [`sync-vscode-config`](#sync-vscode-config)
- [`check-ownership`](#check-ownership)
- [Contributing](#contributing)

Expand Down Expand Up @@ -117,6 +118,15 @@ Check that all TODO comments follow the same pattern and link a Jira ticket: `TO

Check for non existing and duplicate paths in `.pre-commit-config.yaml`. Background: In a big codebase, the exclude lists can be quite long and it's easy to make a typo or forget to remove an entry when it's no longer needed.

### `sync-vscode-config`

Sync VSCode settings and extensions from `devcontainer.json` to `.vscode` folder.
`devcontainer.json` will be now your source of truth.
Entries defined in `settings.json` and `extensions.json` which don't exist in `devcontainer.json` will be left as is.

If `settings.json` and `extensions.json` are ignored in Git, consider running the hook in `post-checkout` and `post-merge` stages by overwriting the `stages` config.
In this case, define your `default_install_hook_types` in the pre-commit config and set `always_run: true` for this hook.

### `check-ownership`

Check if all folders in the `CODEOWNERS` file exist, there are no duplicates, and it has acceptable codeowners.
Expand Down
152 changes: 152 additions & 0 deletions dev_tools/sync_vscode_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#!/usr/bin/env python

# Copyright (c) Luminar Technologies, Inc. All rights reserved.
# Licensed under the MIT License.

from __future__ import annotations

import argparse
import json # for writing JSON, we need a pretty printer. PyJSON5 doesn't support this: https://github.com/Kijewski/pyjson5/issues/19#issuecomment-970504400
import logging
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Sequence

import pyjson5 # for parsing comments, we need JSON5

DEFAULT_INDENT = 4


@dataclass
class DictOverwriteRecord:
key: str
old_value: Any
new_value: Any


def load_devcontainer_config(devcontainer_json_path: Path) -> Any: # noqa: ANN401
return pyjson5.loads(devcontainer_json_path.read_text())["customizations"]["vscode"]


def get_and_set(dict: Any, key: Any, value: Any) -> Any: # noqa: ANN401
old_value = dict.get(key, None)
dict[key] = value
return old_value


def update_dict_overwriting_values(dict: Any, new_values_dict: dict) -> list[DictOverwriteRecord]: # noqa: ANN401
overwrite_records = []
for key, value in new_values_dict.items():
old_value = get_and_set(dict, key, value)
if old_value is not None and old_value != value:
overwrite_records.append(DictOverwriteRecord(key, old_value, value))
return overwrite_records


def combine_lists_without_duplicates(
old_values: Any, # noqa: ANN401
new_values_list: list,
) -> list[str]:
combined_set = set(old_values) | set(new_values_list)
return sorted(combined_set)


def write_vscode_json(json_path: Path, json_dict: dict, indent: int) -> None:
json_path.parent.mkdir(parents=True, exist_ok=True)
json_path.write_text(json.dumps(json_dict, indent=indent, ensure_ascii=False) + "\n")


def update_vscode_settings_json(
settings_json: Path, settings_dict: dict, indent: int = DEFAULT_INDENT
) -> list[DictOverwriteRecord]:
old_settings_dict = pyjson5.loads(settings_json.read_text()) if settings_json.is_file() else {}
overwrite_records = update_dict_overwriting_values(old_settings_dict, settings_dict)
write_vscode_json(settings_json, old_settings_dict, indent=indent)

return overwrite_records


def get_extension_recommendations(extensions_dict: Any) -> list[str]: # noqa: ANN401
recommendations = extensions_dict.get("recommendations", [])
if not isinstance(recommendations, list):
msg = "Invalid settings.json: recommendations must be a list"
raise TypeError(msg)
for item in recommendations:
if not isinstance(item, str):
msg = f"Invalid settings.json: recommendations must be a list of strings. Bad item: '{item}'"
raise TypeError(msg)
return recommendations


def filter_out_unwanted_recommendations(recommendations: list[str]) -> list[str]:
# extensions.json's unwantedRecommendations are not supported yet
return [item for item in recommendations if not item.startswith("-")]


def update_vscode_extensions_json(
extensions_json: Path, extensions_list: list[str], indent: int = DEFAULT_INDENT
) -> None:
old_extensions_dict = pyjson5.loads(extensions_json.read_text()) if extensions_json.is_file() else {}
old_extensions_list = get_extension_recommendations(old_extensions_dict)
old_extensions_dict["recommendations"] = combine_lists_without_duplicates(
old_extensions_list, filter_out_unwanted_recommendations(extensions_list)
)
write_vscode_json(extensions_json, old_extensions_dict, indent=indent)


def report_settings_findings(findings: list[DictOverwriteRecord], settings_json: Path) -> None:
if any(findings):
msg = f"Updated {settings_json}"
logging.info(msg)
for finding in findings:
msg = f"In {settings_json}, '{finding.key}' was overwritten from '{finding.old_value}' to '{finding.new_value}'"
logging.warning(msg)


def parse_arguments(args: Sequence[str] | None = None) -> argparse.Namespace:
repo_root = Path.cwd()
parser = argparse.ArgumentParser(description="Sync VS Code settings and extensions from devcontainer.json")
parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose logging")
parser.add_argument(
"--devcontainer-json",
type=Path,
default=repo_root / ".devcontainer" / "devcontainer.json",
help="Path to devcontainer.json",
)
parser.add_argument(
"--settings-path",
type=Path,
default=repo_root / ".vscode" / "settings.json",
help="Path to settings.json which will contain merged settings",
)
parser.add_argument(
"--extensions-path",
type=Path,
default=repo_root / ".vscode" / "extensions.json",
help="Path to extensions.json which will contain merged settings",
)
parser.add_argument("--indent", type=int, default=DEFAULT_INDENT, help="Indentation level for JSON output")
return parser.parse_args(args)


def main() -> int:
args = parse_arguments()
lvl = logging.INFO if args.verbose else logging.WARNING
logging.basicConfig(level=lvl, format="%(asctime)s [%(levelname)s] %(message)s")

msg = f"Syncing VS Code settings and extensions from {args.devcontainer_json} to {args.settings_path} and {args.extensions_path}"
logging.info(msg)

devcontainer_config = load_devcontainer_config(args.devcontainer_json)
settings_findings = update_vscode_settings_json(
args.settings_path, devcontainer_config.get("settings", {}), indent=args.indent
)
update_vscode_extensions_json(args.extensions_path, devcontainer_config.get("extensions", []))
report_settings_findings(settings_findings, args.settings_path)

return 0


if __name__ == "__main__":
sys.exit(main())
Loading

0 comments on commit 5d6755e

Please sign in to comment.