Skip to content

Commit

Permalink
feat(nx-python): add poetry publish executor (#241)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasvieirasilva authored Aug 8, 2024
1 parent 0db96e5 commit f34b797
Show file tree
Hide file tree
Showing 10 changed files with 1,384 additions and 146 deletions.
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

0 comments on commit f34b797

Please sign in to comment.