diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d92754..ffcc011 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Implement initial PythonTA LSP server - Add initial PythonTA implementation that simply calls the CLI - Lint Python documents from their editor contents through standard input +- Add automatic discovery of PythonTA configuration files in the workspace root ### 🐛 Bug fixes diff --git a/README.md b/README.md index b3161f4..1d8b857 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,14 @@ This project is an extension for running [PythonTA](https://www.cs.toronto.edu/~david/pyta/) within VS Code. +## Configuration + +By default, `python-ta.useProjectRootConfig` automatically uses a PythonTA configuration file from the root of the current workspace. +The extension checks for `.pylintrc`, `pylintrc`, and `pyproject.toml`, in that order. + +When project-root configuration discovery is enabled, `python-ta.configPath` is ignored. +When discovery is disabled, `python-ta.configPath` specifies the configuration file instead. + ## Developers ### Requirements diff --git a/bundled/tool/lsp_server.py b/bundled/tool/lsp_server.py index 62b820e..183cfa7 100644 --- a/bundled/tool/lsp_server.py +++ b/bundled/tool/lsp_server.py @@ -85,6 +85,7 @@ def update_sys_path(path_to_add: str, strategy: str) -> None: TOOL_DISPLAY = "PythonTA" TOOL_ARGS = ["--output-format", "pyta-lsp", "--exit-zero"] # default arguments always passed to your tool +PROJECT_ROOT_CONFIG_FILENAMES = (".pylintrc", "pylintrc", "pyproject.toml") # TODO: If your tool is a linter then update this section. # Delete "Linting features" section if your tool is NOT a linter. @@ -387,6 +388,7 @@ def _get_global_defaults(): "importStrategy": GLOBAL_SETTINGS.get("importStrategy", "useBundled"), "showNotifications": GLOBAL_SETTINGS.get("showNotifications", "off"), "configPath": GLOBAL_SETTINGS.get("configPath", ""), + "useProjectRootConfig": GLOBAL_SETTINGS.get("useProjectRootConfig", True), } @@ -455,6 +457,27 @@ def _get_settings_by_document(document: workspace.TextDocument | None): return WORKSPACE_SETTINGS[str(key)] +def _find_project_root_config(project_root: str, config_filenames: Sequence[str]) -> str | None: + """Return the first config file from inputted filenames that's found in the project root.""" + root = pathlib.Path(project_root) + for filename in config_filenames: + config_path = root / filename + if config_path.is_file(): + return os.fspath(config_path) + + return None + + +def _get_config_path(settings: dict[str, Any]) -> str | None: + """Return the discovered project-root config or the configured path.""" + if settings.get("useProjectRootConfig", True): + return _find_project_root_config( + settings["workspaceFS"], PROJECT_ROOT_CONFIG_FILENAMES + ) + + return settings.get("configPath") or None + + # ***************************************************** # Internal execution APIs. # ***************************************************** @@ -507,7 +530,7 @@ def _run_tool_on_document( argv += TOOL_ARGS + settings["args"] + extra_args - config_path = settings.get("configPath") + config_path = _get_config_path(settings) if config_path: argv += ["--config", config_path] diff --git a/package.json b/package.json index 975a239..f056253 100644 --- a/package.json +++ b/package.json @@ -69,10 +69,18 @@ "contributes": { "configuration": { "properties": { + "python-ta.useProjectRootConfig": { + "type": "boolean", + "default": true, + "description": "Automatically use a supported PythonTA configuration file from the workspace root. When enabled, python-ta.configPath is ignored.", + "order": 1, + "scope": "resource" + }, "python-ta.configPath": { "type": "string", "default": "", - "description": "Absolute or relative path to your PythonTA configuration file (e.g., .pyta_config.ini).", + "description": "Absolute or relative path to your PythonTA configuration file (e.g., .pyta_config.ini). This setting is ignored when python-ta.useProjectRootConfig is enabled.", + "order": 2, "scope": "resource" }, "python-ta.cwd": { diff --git a/src/common/settings.ts b/src/common/settings.ts index 831309c..b16fd37 100644 --- a/src/common/settings.ts +++ b/src/common/settings.ts @@ -11,6 +11,7 @@ export interface ISettings { importStrategy: string; showNotifications: string; configPath: string; + useProjectRootConfig: boolean; } export function getExtensionSettings(namespace: string, includeInterpreter?: boolean): Promise { @@ -73,6 +74,7 @@ export async function getWorkspaceSettings( importStrategy: config.get(`importStrategy`) ?? 'useBundled', showNotifications: config.get(`showNotifications`) ?? 'off', configPath: config.get('configPath', ''), + useProjectRootConfig: config.get('useProjectRootConfig', true), }; return workspaceSetting; } @@ -102,6 +104,7 @@ export async function getGlobalSettings(namespace: string, includeInterpreter?: importStrategy: getGlobalValue(config, 'importStrategy', 'useBundled'), showNotifications: getGlobalValue(config, 'showNotifications', 'off'), configPath: getGlobalValue(config, 'configPath', ''), + useProjectRootConfig: getGlobalValue(config, 'useProjectRootConfig', true), }; return setting; } @@ -115,6 +118,7 @@ export function checkIfConfigurationChanged(e: ConfigurationChangeEvent, namespa `${namespace}.importStrategy`, `${namespace}.showNotifications`, `${namespace}.configPath`, + `${namespace}.useProjectRootConfig`, ]; const changed = settings.map((s) => e.affectsConfiguration(s)); return changed.includes(true); diff --git a/src/test/python_tests/test_lsp_server_helpers.py b/src/test/python_tests/test_lsp_server_helpers.py index 73aa079..4414ce6 100644 --- a/src/test/python_tests/test_lsp_server_helpers.py +++ b/src/test/python_tests/test_lsp_server_helpers.py @@ -143,6 +143,53 @@ def test_parse_json_output_returns_empty_list_for_file_with_no_diagnostics(): assert result == [] +# --------------------------------------------------------------------------- +# Project-root config discovery and selection +# --------------------------------------------------------------------------- + + +def test_get_config_path_uses_pyta_filename_priority_when_enabled(tmp_path): + (tmp_path / "pyproject.toml").touch() + (tmp_path / "pylintrc").touch() + highest_priority = tmp_path / ".pylintrc" + highest_priority.touch() + settings = { + "useProjectRootConfig": True, + "configPath": os.fspath(tmp_path / "manual.toml"), + "workspaceFS": os.fspath(tmp_path), + } + + result = lsp_server._get_config_path(settings) + + assert result == os.fspath(highest_priority) + + +def test_get_config_path_ignores_config_path_when_discovery_finds_nothing(tmp_path): + settings = { + "useProjectRootConfig": True, + "configPath": os.fspath(tmp_path / "manual.toml"), + "workspaceFS": os.fspath(tmp_path), + } + + result = lsp_server._get_config_path(settings) + + assert result is None + + +def test_get_config_path_uses_config_path_when_discovery_is_disabled(tmp_path): + (tmp_path / ".pylintrc").touch() + manual_config = tmp_path / "manual.toml" + settings = { + "useProjectRootConfig": False, + "configPath": os.fspath(manual_config), + "workspaceFS": os.fspath(tmp_path), + } + + result = lsp_server._get_config_path(settings) + + assert result == os.fspath(manual_config) + + # --------------------------------------------------------------------------- # _get_global_defaults # --------------------------------------------------------------------------- @@ -155,6 +202,7 @@ def test_get_global_defaults_uses_builtin_fallbacks_when_empty(): assert defaults["importStrategy"] == "useBundled" assert defaults["showNotifications"] == "off" assert defaults["configPath"] == "" + assert defaults["useProjectRootConfig"] is True def test_get_global_defaults_reflects_global_settings_overrides(): diff --git a/src/test/suite/settings.test.ts b/src/test/suite/settings.test.ts index 42287a6..f4cf03e 100644 --- a/src/test/suite/settings.test.ts +++ b/src/test/suite/settings.test.ts @@ -12,6 +12,7 @@ suite('settings.getGlobalSettings', () => { assert.strictEqual(settings.importStrategy, 'useBundled'); assert.strictEqual(settings.showNotifications, 'off'); assert.strictEqual(settings.configPath, ''); + assert.strictEqual(settings.useProjectRootConfig, true); }); });