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

feat(nx-python): add poetry publish executor #241

Merged
merged 1 commit into from
Aug 8, 2024
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,184 changes: 1,043 additions & 141 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
"@types/node": "18.19.21",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"@vitest/coverage-v8": "^1.0.4",
"@vitest/ui": "^1.3.1",
"@vitest/coverage-v8": "^1.6.0",
"@vitest/ui": "^1.6.0",
"aws-sdk-client-mock": "^3.0.1",
"aws-sdk-client-mock-jest": "^3.0.1",
"commitizen": "^4.3.0",
Expand All @@ -56,7 +56,7 @@
"ts-node": "10.9.2",
"typescript": "5.3.3",
"vite": "~5.0.0",
"vitest": "^1.3.1"
"vitest": "^1.6.0"
},
"dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.525.0",
Expand Down
16 changes: 16 additions & 0 deletions packages/nx-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,22 @@ The `@nxlv/python:install` handles the `poetry install` command for a project.
| `--verbose` | `boolean` | Use verbose mode in the install `poetry install -vv` | `false` | `false` |
| `--debug` | `boolean` | Use debug mode in the install `poetry install -vvv` | `false` | `false` |

#### publish

The `@nxlv/python:publish` executor handles the `poetry publish` command for a project.

#### Options

| Option | Type | Description | Required | Default |
| --------------- | :-------: | ----------------------------------------------------------------------------------- | -------- | ------- |
| `--silent` | `boolean` | Hide output text | `false` | `false` |
| `--buildTarget` | `string` | Build Nx target (it needs to a target that uses the `@nxlv/python:build` execution) | `false` | `build` |

This executor first executes the `build` target to generate the tar/whl files and uses the `--keepBuildFolder` flag to keep the build folder after the build process.

For must scenarios, running the `poetry publish` with `@nxlv/python:run-commands` executor is enough,
however, when the project has local dependencies and the `--bundleLocalDependencies=false` option is used, the default `poetry publish` command doesn't work properly, because the `poetry publish` command uses the current `pyproject.toml` file, which doesn't have the local dependencies resolved, the `@nxlv/python:publish` executor solves this issue by running the `poetry publish` command inside the temporary build folder generated by the `@nxlv/python:build` executor, so, the `pyproject.toml` file has all the dependencies resolved.

#### run-commands (same as `nx:run-commands`)

The `@nxlv/python:run-commands` wraps the `nx:run-commands` default Nx executor and if the `autoActivate` option is set to `true` in the root `pyproject.toml` file, it will verify the the virtual environment is not activated, if no, it will activate the virtual environment before running the commands.
Expand Down
5 changes: 5 additions & 0 deletions packages/nx-python/executors.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
"schema": "./src/executors/build/schema.json",
"description": "build executor"
},
"publish": {
"implementation": "./src/executors/publish/executor",
"schema": "./src/executors/publish/schema.json",
"description": "publish executor"
},
"add": {
"implementation": "./src/executors/add/executor",
"schema": "./src/executors/add/schema.json",
Expand Down
6 changes: 4 additions & 2 deletions packages/nx-python/src/executors/build/executor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ExecutorContext } from '@nx/devkit';
import { BuildExecutorSchema } from './schema';
import { BuildExecutorOutput, BuildExecutorSchema } from './schema';
import {
readdirSync,
copySync,
Expand Down Expand Up @@ -34,7 +34,7 @@ const logger = new Logger();
export default async function executor(
options: BuildExecutorSchema,
context: ExecutorContext,
) {
): Promise<BuildExecutorOutput> {
logger.setOptions(options);
const workspaceRoot = context.root;
process.chdir(workspaceRoot);
Expand Down Expand Up @@ -123,11 +123,13 @@ export default async function executor(
}

return {
buildFolderPath,
success: true,
};
} catch (error) {
logger.info(chalk`\n {bgRed.bold ERROR } ${error.message}\n`);
return {
buildFolderPath: '',
success: false,
};
}
Expand Down
5 changes: 5 additions & 0 deletions packages/nx-python/src/executors/build/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ export interface BuildExecutorSchema {
customSourceUrl?: string;
publish?: boolean;
}

export interface BuildExecutorOutput {
buildFolderPath: string;
success: boolean;
}
204 changes: 204 additions & 0 deletions packages/nx-python/src/executors/publish/executor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { vi, MockInstance } from 'vitest';

const fsExtraMocks = vi.hoisted(() => {
return {
removeSync: vi.fn(),
};
});

const nxDevkitMocks = vi.hoisted(() => {
return {
runExecutor: vi.fn(),
};
});

vi.mock('@nx/devkit', async (importOriginal) => {
const actual = await importOriginal<typeof import('@nx/devkit')>();
return {
...actual,
...nxDevkitMocks,
};
});

vi.mock('fs-extra', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs-extra')>();
return {
...actual,
...fsExtraMocks,
};
});

import chalk from 'chalk';
import '../../utils/mocks/cross-spawn.mock';
import * as poetryUtils from '../utils/poetry';
import executor from './executor';
import spawn from 'cross-spawn';

describe('Publish Executor', () => {
let checkPoetryExecutableMock: MockInstance;
let activateVenvMock: MockInstance;

const context = {
cwd: '',
root: '.',
isVerbose: false,
projectName: 'app',
workspace: {
version: 2,
projects: {
app: {
root: 'apps/app',
targets: {},
},
},
},
};

beforeEach(() => {
checkPoetryExecutableMock = vi
.spyOn(poetryUtils, 'checkPoetryExecutable')
.mockResolvedValue(undefined);

activateVenvMock = vi
.spyOn(poetryUtils, 'activateVenv')
.mockReturnValue(undefined);

vi.mocked(spawn.sync).mockReturnValue({
status: 0,
output: [''],
pid: 0,
signal: null,
stderr: null,
stdout: null,
});

vi.spyOn(process, 'chdir').mockReturnValue(undefined);
});

beforeAll(() => {
console.log(chalk`init chalk`);
});

afterEach(() => {
vi.resetAllMocks();
});

it('should return success false when the poetry is not installed', async () => {
checkPoetryExecutableMock.mockRejectedValue(new Error('poetry not found'));

const options = {
buildTarget: 'build',
silent: false,
};

const output = await executor(options, context);
expect(checkPoetryExecutableMock).toHaveBeenCalled();
expect(activateVenvMock).toHaveBeenCalledWith('.');
expect(spawn.sync).not.toHaveBeenCalled();
expect(output.success).toBe(false);
});

it('should return success false when the build target fails', async () => {
nxDevkitMocks.runExecutor.mockResolvedValueOnce([{ success: false }]);

const options = {
buildTarget: 'build',
silent: false,
};

const output = await executor(options, context);
expect(checkPoetryExecutableMock).toHaveBeenCalled();
expect(activateVenvMock).toHaveBeenCalledWith('.');
expect(spawn.sync).not.toHaveBeenCalled();
expect(output.success).toBe(false);
});

it('should return success false when the build target does not return the temp folder', async () => {
nxDevkitMocks.runExecutor.mockResolvedValueOnce([{ success: true }]);

const options = {
buildTarget: 'build',
silent: false,
__unparsed__: [],
};

const output = await executor(options, context);
expect(checkPoetryExecutableMock).toHaveBeenCalled();
expect(activateVenvMock).toHaveBeenCalledWith('.');
expect(spawn.sync).not.toHaveBeenCalled();
expect(output.success).toBe(false);
});

it('should run poetry publish command without agrs', async () => {
nxDevkitMocks.runExecutor.mockResolvedValueOnce([
{ success: true, buildFolderPath: 'tmp' },
]);
fsExtraMocks.removeSync.mockReturnValue(undefined);

const options = {
buildTarget: 'build',
silent: false,
};

const output = await executor(options, context);
expect(checkPoetryExecutableMock).toHaveBeenCalled();
expect(activateVenvMock).toHaveBeenCalledWith('.');
expect(spawn.sync).toHaveBeenCalledWith('poetry', ['publish'], {
cwd: 'tmp',
shell: false,
stdio: 'inherit',
});
expect(output.success).toBe(true);
expect(nxDevkitMocks.runExecutor).toHaveBeenCalledWith(
{
configuration: undefined,
project: 'app',
target: 'build',
},
{
keepBuildFolder: true,
},
context,
);
expect(fsExtraMocks.removeSync).toHaveBeenCalledWith('tmp');
});

it('should run poetry publish command with agrs', async () => {
nxDevkitMocks.runExecutor.mockResolvedValueOnce([
{ success: true, buildFolderPath: 'tmp' },
]);
fsExtraMocks.removeSync.mockReturnValue(undefined);

const options = {
buildTarget: 'build',
silent: false,
__unparsed__: ['-vvv', '--dry-run'],
};

const output = await executor(options, context);
expect(checkPoetryExecutableMock).toHaveBeenCalled();
expect(activateVenvMock).toHaveBeenCalledWith('.');
expect(spawn.sync).toHaveBeenCalledWith(
'poetry',
['publish', '-vvv', '--dry-run'],
{
cwd: 'tmp',
shell: false,
stdio: 'inherit',
},
);
expect(output.success).toBe(true);
expect(nxDevkitMocks.runExecutor).toHaveBeenCalledWith(
{
configuration: undefined,
project: 'app',
target: 'build',
},
{
keepBuildFolder: true,
},
context,
);
expect(fsExtraMocks.removeSync).toHaveBeenCalledWith('tmp');
});
});
69 changes: 69 additions & 0 deletions packages/nx-python/src/executors/publish/executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { ExecutorContext, runExecutor } from '@nx/devkit';
import { PublishExecutorSchema } from './schema';
import chalk from 'chalk';
import { Logger } from '../utils/logger';
import {
activateVenv,
checkPoetryExecutable,
runPoetry,
} from '../utils/poetry';
import { BuildExecutorOutput } from '../build/schema';
import { removeSync } from 'fs-extra';

const logger = new Logger();

export default async function executor(
options: PublishExecutorSchema,
context: ExecutorContext,
) {
logger.setOptions(options);
const workspaceRoot = context.root;
process.chdir(workspaceRoot);
try {
activateVenv(workspaceRoot);
await checkPoetryExecutable();

let buildFolderPath = '';

for await (const output of await runExecutor<BuildExecutorOutput>(
{
project: context.projectName,
target: options.buildTarget,
configuration: context.configurationName,
},
{
keepBuildFolder: true,
},
context,
)) {
if (!output.success) {
throw new Error('Build failed');
}

buildFolderPath = output.buildFolderPath;
}

if (!buildFolderPath) {
throw new Error('Cannot find the temporary build folder');
}

logger.info(
chalk`\n {bold Publishing project {bgBlue ${context.projectName} }...}\n`,
);

await runPoetry(['publish', ...(options.__unparsed__ ?? [])], {
cwd: buildFolderPath,
});

removeSync(buildFolderPath);

return {
success: true,
};
} catch (error) {
logger.info(chalk`\n {bgRed.bold ERROR } ${error.message}\n`);
return {
success: false,
};
}
}
5 changes: 5 additions & 0 deletions packages/nx-python/src/executors/publish/schema.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface PublishExecutorSchema {
silent: boolean;
buildTarget: string;
__unparsed__?: string[];
}
Loading
Loading