Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 24 additions & 1 deletion bundled/tool/lsp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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),
}


Expand Down Expand Up @@ -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.
# *****************************************************
Expand Down Expand Up @@ -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]

Expand Down
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
4 changes: 4 additions & 0 deletions src/common/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface ISettings {
importStrategy: string;
showNotifications: string;
configPath: string;
useProjectRootConfig: boolean;
}

export function getExtensionSettings(namespace: string, includeInterpreter?: boolean): Promise<ISettings[]> {
Expand Down Expand Up @@ -73,6 +74,7 @@ export async function getWorkspaceSettings(
importStrategy: config.get<string>(`importStrategy`) ?? 'useBundled',
showNotifications: config.get<string>(`showNotifications`) ?? 'off',
configPath: config.get<string>('configPath', ''),
useProjectRootConfig: config.get<boolean>('useProjectRootConfig', true),
};
return workspaceSetting;
}
Expand Down Expand Up @@ -102,6 +104,7 @@ export async function getGlobalSettings(namespace: string, includeInterpreter?:
importStrategy: getGlobalValue<string>(config, 'importStrategy', 'useBundled'),
showNotifications: getGlobalValue<string>(config, 'showNotifications', 'off'),
configPath: getGlobalValue<string>(config, 'configPath', ''),
useProjectRootConfig: getGlobalValue<boolean>(config, 'useProjectRootConfig', true),
};
return setting;
}
Expand All @@ -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);
Expand Down
48 changes: 48 additions & 0 deletions src/test/python_tests/test_lsp_server_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand All @@ -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():
Expand Down
1 change: 1 addition & 0 deletions src/test/suite/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

Expand Down
Loading