Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional Python and R attributes within configuration file #2587

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 3 additions & 1 deletion cmd/publisher/commands/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/posit-dev/publisher/internal/deployment"
"github.com/posit-dev/publisher/internal/events"
"github.com/posit-dev/publisher/internal/initialize"
"github.com/posit-dev/publisher/internal/logging"
"github.com/posit-dev/publisher/internal/publish"
"github.com/posit-dev/publisher/internal/state"
"github.com/posit-dev/publisher/internal/util"
Expand All @@ -28,6 +29,7 @@ type DeployCmd struct {
}

func (cmd *DeployCmd) Run(args *cli_types.CommonArgs, ctx *cli_types.CLIContext) error {
log := logging.New()
absPath, err := cmd.Path.Abs()
if err != nil {
return err
Expand Down Expand Up @@ -58,7 +60,7 @@ func (cmd *DeployCmd) Run(args *cli_types.CommonArgs, ctx *cli_types.CLIContext)
if err != nil {
return err
}
stateStore, err := state.New(absPath, cmd.AccountName, cmd.ConfigName, "", cmd.SaveName, ctx.Accounts, nil, false)
stateStore, err := state.New(absPath, cmd.AccountName, cmd.ConfigName, "", cmd.SaveName, ctx.Accounts, nil, false, nil, nil, log)
if err != nil {
return err
}
Expand Down
4 changes: 3 additions & 1 deletion cmd/publisher/commands/redeploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/posit-dev/publisher/internal/deployment"
"github.com/posit-dev/publisher/internal/events"
"github.com/posit-dev/publisher/internal/initialize"
"github.com/posit-dev/publisher/internal/logging"
"github.com/posit-dev/publisher/internal/publish"
"github.com/posit-dev/publisher/internal/state"
"github.com/posit-dev/publisher/internal/util"
Expand All @@ -27,6 +28,7 @@ type RedeployCmd struct {
}

func (cmd *RedeployCmd) Run(args *cli_types.CommonArgs, ctx *cli_types.CLIContext) error {
log := logging.New()
absPath, err := cmd.Path.Abs()
if err != nil {
return err
Expand All @@ -43,7 +45,7 @@ func (cmd *RedeployCmd) Run(args *cli_types.CommonArgs, ctx *cli_types.CLIContex
if err != nil {
return fmt.Errorf("invalid deployment name '%s': %w", cmd.TargetName, err)
}
stateStore, err := state.New(absPath, "", cmd.ConfigName, cmd.TargetName, "", ctx.Accounts, nil, false)
stateStore, err := state.New(absPath, "", cmd.ConfigName, cmd.TargetName, "", ctx.Accounts, nil, false, nil, nil, log)
if err != nil {
return err
}
Expand Down
3 changes: 3 additions & 0 deletions extensions/vscode/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Credentials } from "./resources/Credentials";
import { ContentRecords } from "./resources/ContentRecords";
import { Configurations } from "./resources/Configurations";
import { Files } from "./resources/Files";
import { Interpreters } from "./resources/Interpreters";
import { Packages } from "./resources/Packages";
import { Secrets } from "./resources/Secrets";
import { EntryPoints } from "./resources/Entrypoints";
Expand All @@ -15,6 +16,7 @@ class PublishingClientApi {
private client;

configurations: Configurations;
interpreters: Interpreters;
credentials: Credentials;
contentRecords: ContentRecords;
files: Files;
Expand Down Expand Up @@ -52,6 +54,7 @@ class PublishingClientApi {
this.credentials = new Credentials(this.client);
this.contentRecords = new ContentRecords(this.client);
this.files = new Files(this.client);
this.interpreters = new Interpreters(this.client);
this.packages = new Packages(this.client);
this.secrets = new Secrets(this.client);
this.entrypoints = new EntryPoints(this.client);
Expand Down
12 changes: 6 additions & 6 deletions extensions/vscode/src/api/resources/Configurations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ConfigurationError,
ConfigurationInspectionResult,
} from "../types/configurations";
import { PythonExecutable, RExecutable } from "../../types/shared";

export class Configurations {
private client: AxiosInstance;
Expand Down Expand Up @@ -80,19 +81,18 @@ export class Configurations {
// 500 - internal server error
inspect(
dir: string,
python?: string,
r?: string,
python: PythonExecutable | undefined,
r: RExecutable | undefined,
Comment on lines +84 to +85
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could keep using ?, a bit simpler

python?: PythonExecutable,
r?: RExecutable,

params?: { entrypoint?: string; recursive?: boolean },
) {
return this.client.post<ConfigurationInspectionResult[]>(
"/inspect",
{
python,
r,
},
{},
{
params: {
dir,
python: python !== undefined ? python.pythonPath : undefined,
r: r !== undefined ? r.rPath : "",
Comment on lines +94 to +95
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and here

python: python ? python.pythonPath : undefined,
r: r ? r.rPath : "",

Same goes for all the other API methods below

...params,
},
},
Expand Down
9 changes: 5 additions & 4 deletions extensions/vscode/src/api/resources/ContentRecords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ContentRecord,
Environment,
} from "../types/contentRecords";
import { PythonExecutable, RExecutable } from "../../types/shared";

export class ContentRecords {
private client: AxiosInstance;
Expand Down Expand Up @@ -74,17 +75,15 @@ export class ContentRecords {
configName: string,
insecure: boolean,
dir: string,
r: RExecutable | undefined,
python: PythonExecutable | undefined,
secrets?: Record<string, string>,
r?: string,
python?: string,
) {
const data = {
account: accountName,
config: configName,
secrets: secrets,
insecure: insecure,
r: r,
python: python,
};
const encodedTarget = encodeURIComponent(targetName);
return this.client.post<{ localId: string }>(
Expand All @@ -93,6 +92,8 @@ export class ContentRecords {
{
params: {
dir,
r: r !== undefined ? r.rPath : "",
python: python !== undefined ? python.pythonPath : "",
},
},
);
Expand Down
31 changes: 31 additions & 0 deletions extensions/vscode/src/api/resources/Interpreters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (C) 2025 by Posit Software, PBC.

import { AxiosInstance } from "axios";

import { PythonExecutable, RExecutable } from "../../types/shared";
import { InterpreterDefaults } from "../types/interpreters";

export class Interpreters {
private client: AxiosInstance;

constructor(client: AxiosInstance) {
this.client = client;
}

// Returns:
// 200 - success
// 500 - internal server error
get(
dir: string,
r: RExecutable | undefined,
python: PythonExecutable | undefined,
) {
return this.client.get<InterpreterDefaults>(`/interpreters`, {
params: {
dir,
r: r !== undefined ? r.rPath : "",
python: python !== undefined ? python.pythonPath : "",
},
});
}
}
31 changes: 25 additions & 6 deletions extensions/vscode/src/api/resources/Packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
PythonPackagesResponse,
ScanPythonPackagesResponse,
} from "../types/packages";
import { PythonExecutable, RExecutable } from "../../types/shared";

export class Packages {
private client: AxiosInstance;
Expand Down Expand Up @@ -48,25 +49,43 @@ export class Packages {
// 500 - internal server error
createPythonRequirementsFile(
dir: string,
python?: string,
python: PythonExecutable | undefined,
saveName?: string,
) {
return this.client.post<ScanPythonPackagesResponse>(
"packages/python/scan",
{ python, saveName },
{ params: { dir } },
{
saveName,
},
{
params: {
dir,
python: python !== undefined ? python.pythonPath : undefined,
},
},
);
}

// Returns:
// 200 - success
// 400 - bad request
// 500 - internal server error
createRRequirementsFile(dir: string, saveName?: string, r?: string) {
createRRequirementsFile(
dir: string,
r: RExecutable | undefined,
saveName?: string,
) {
return this.client.post<void>(
"packages/r/scan",
{ saveName, r },
{ params: { dir } },
{
saveName,
},
{
params: {
dir,
r: r !== undefined ? r.rPath : "",
},
},
);
}
}
46 changes: 46 additions & 0 deletions extensions/vscode/src/api/types/configurations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { AgentError } from "./error";
import { ConnectConfig } from "./connect";
import { SchemaURL } from "./schema";
import { InterpreterDefaults } from "./interpreters";

export type ConfigurationLocation = {
configurationName: string;
Expand Down Expand Up @@ -176,3 +177,48 @@ export type Group = {
name?: string;
permissions: string;
};

export function UpdateAllConfigsWithDefaults(
configs: (Configuration | ConfigurationError)[],
defaults: InterpreterDefaults,
) {
for (let i = 0; i < configs.length; i++) {
configs[i] = UpdateConfigWithDefaults(configs[i], defaults);
}
return configs;
}

export function UpdateConfigWithDefaults(
config: Configuration | ConfigurationError,
defaults: InterpreterDefaults,
) {
if (isConfigurationError(config)) {
return config;
}

// Fill in empty definitions with the current defaults
// but only if the section is defined (which indicates the dependency)
if (config.configuration.r !== undefined) {
if (config.configuration.r.version === "") {
config.configuration.r.version = defaults.r.version;
}
if (config.configuration.r.packageFile === "") {
config.configuration.r.packageFile = defaults.r.packageFile;
}
if (config.configuration.r.packageManager === "") {
config.configuration.r.packageManager = defaults.r.packageManager;
}
}
if (config.configuration.python !== undefined) {
if (config.configuration.python.version === "") {
config.configuration.python.version = defaults.r.version;
}
if (config.configuration.python.packageFile === "") {
config.configuration.python.packageFile = defaults.r.packageFile;
}
if (config.configuration.python.packageManager === "") {
config.configuration.python.packageManager = defaults.r.packageManager;
}
}
return config;
}
10 changes: 10 additions & 0 deletions extensions/vscode/src/api/types/interpreters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (C) 2025 by Posit Software, PBC.

import { PythonConfig, RConfig } from "./configurations";

export type InterpreterDefaults = {
python: PythonConfig;
preferredPythonPath: string;
r: RConfig;
preferredRPath: string;
};
20 changes: 19 additions & 1 deletion extensions/vscode/src/state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ class mockApiClient {
list: vi.fn(),
reset: vi.fn(),
};

readonly interpreters = {
get: vi.fn(() => {
return {
data: {
dir: "/usr/proj",
r: "/usr/bin/r",
python: "/usr/bin/python",
},
};
}),
};
}

const mockClient = new mockApiClient();
Expand Down Expand Up @@ -61,9 +73,14 @@ vi.mock("vscode", () => {
showInformationMessage: vi.fn(),
};

const workspaceStateMock = {
get: vi.fn(),
};

return {
Disposable: disposableMock,
window: windowMock,
workspace: workspaceStateMock,
};
});

Expand Down Expand Up @@ -249,11 +266,12 @@ describe("PublisherState", () => {
const contentRecordState: DeploymentSelectorState =
selectionStateFactory.build();

const { mockContext } = mkExtensionContextStateMock({});
const { mockContext, mockWorkspace } = mkExtensionContextStateMock({});
const publisherState = new PublisherState(mockContext);

// No config get due to no content record set
let currentConfig = await publisherState.getSelectedConfiguration();
expect(mockWorkspace.get).toHaveBeenCalled();
expect(currentConfig).toEqual(undefined);
expect(mockClient.configurations.get).not.toHaveBeenCalled();

Expand Down
Loading
Loading