diff --git a/packages/nx-python/src/dependency/update-dependency.ts b/packages/nx-python/src/dependency/update-dependency.ts index e80ad37..9425651 100644 --- a/packages/nx-python/src/dependency/update-dependency.ts +++ b/packages/nx-python/src/dependency/update-dependency.ts @@ -41,7 +41,7 @@ export function updateDependencyTree(context: ExecutorContext) { ); runPoetry(['lock', '--no-update']); - runPoetry(['install']); + runPoetry(['install', '--no-root']); } } } diff --git a/packages/nx-python/src/executors/add/executor.spec.ts b/packages/nx-python/src/executors/add/executor.spec.ts index 5d477fb..1ea35c7 100644 --- a/packages/nx-python/src/executors/add/executor.spec.ts +++ b/packages/nx-python/src/executors/add/executor.spec.ts @@ -1060,10 +1060,15 @@ version = "1.0.0" stdio: 'inherit', }, ); - expect(spawn.sync).toHaveBeenNthCalledWith(3, 'poetry', ['install'], { - shell: false, - stdio: 'inherit', - }); + expect(spawn.sync).toHaveBeenNthCalledWith( + 3, + 'poetry', + ['install', '--no-root'], + { + shell: false, + stdio: 'inherit', + }, + ); expect(output.success).toBe(true); }); @@ -1138,10 +1143,15 @@ version = "1.0.0" stdio: 'inherit', }, ); - expect(spawn.sync).toHaveBeenNthCalledWith(3, 'poetry', ['install'], { - shell: false, - stdio: 'inherit', - }); + expect(spawn.sync).toHaveBeenNthCalledWith( + 3, + 'poetry', + ['install', '--no-root'], + { + shell: false, + stdio: 'inherit', + }, + ); expect(output.success).toBe(true); }); }); diff --git a/packages/nx-python/src/executors/build/executor.spec.ts b/packages/nx-python/src/executors/build/executor.spec.ts index b9f9493..ddc775d 100644 --- a/packages/nx-python/src/executors/build/executor.spec.ts +++ b/packages/nx-python/src/executors/build/executor.spec.ts @@ -312,6 +312,338 @@ describe('Build Executor', () => { expect(output.success).toBe(true); }); + it('should build python project with local dependencies poetry-plugin-export@1.8.0', async () => { + fsMock({ + 'apps/app/.venv/pyvenv.cfg': 'fake', + 'apps/app/app/index.py': 'print("Hello from app")', + 'apps/app/poetry.lock': dedent` + [[package]] + name = "click" + version = "7.1.2" + description = "Composable command line interface toolkit" + category = "main" + optional = false + python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + + [[package]] + name = "dep1" + version = "1.0.0" + description = "Dep1" + category = "main" + optional = false + python-versions = "^3.8" + develop = false + + [package.dependencies] + numpy = "1.21.0" + + [package.source] + type = "directory" + url = "../../libs/dep1" + + [[package]] + name = "numpy" + version = "1.21.0" + description = "NumPy is the fundamental package for array computing with Python." + category = "main" + optional = false + python-versions = ">=3.7" + `, + + 'apps/app/pyproject.toml': dedent` + [tool.poetry] + name = "app" + version = "1.0.0" + [[tool.poetry.packages]] + include = "app" + + [tool.poetry.dependencies] + python = "^3.8" + click = "7.1.2" + dep1 = { path = "../../libs/dep1" } + + [tool.poetry.group.dev.dependencies] + pytest = "6.2.4" + `, + + 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', + 'libs/dep1/pyproject.toml': dedent` + [tool.poetry] + name = "dep1" + version = "1.0.0" + [[tool.poetry.packages]] + include = "dep1" + + [tool.poetry.dependencies] + python = "^3.8" + numpy = "1.21.0" + + [tool.poetry.group.dev.dependencies] + pytest = "6.2.4" + `, + + 'libs/dep2/dep2/index.py': 'print("Hello from dep2")', + 'libs/dep2/pyproject.toml': dedent` + [tool.poetry] + name = "dep2" + version = "1.0.0" + [[tool.poetry.packages]] + include = "dep2" + + [tool.poetry.dependencies] + python = "^3.8" + + [tool.poetry.group.dev.dependencies] + pytest = "6.2.4" + `, + }); + + vi.mocked(spawn.sync).mockImplementation((_, args, opts) => { + if (args[0] == 'build') { + spawnBuildMockImpl(opts); + } else if (args[0] == 'export' && opts.cwd === 'apps/app') { + writeFileSync( + join(buildPath, 'requirements.txt'), + dedent` + click==7.1.2 + -e file://${process.cwd()}/libs/dep1 + numpy==1.21.0; python_version >= "3.8" and python_version < "4.0" + + `, + ); + } + return { + status: 0, + output: [''], + pid: 0, + signal: null, + stderr: null, + stdout: null, + }; + }); + + const options: BuildExecutorSchema = { + ignorePaths: ['.venv', '.tox', 'tests/'], + silent: false, + outputPath: 'dist/apps/app', + keepBuildFolder: true, + devDependencies: false, + lockedVersions: true, + bundleLocalDependencies: true, + }; + + const output = await executor(options, { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + workspace: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + dep1: { + root: 'libs/dep1', + targets: {}, + }, + dep2: { + root: 'libs/dep2', + targets: {}, + }, + }, + }, + }); + + expect(checkPoetryExecutableMock).toHaveBeenCalled(); + expect(activateVenvMock).toHaveBeenCalledWith('.'); + expect(existsSync(buildPath)).toBeTruthy(); + expect(existsSync(`${buildPath}/app`)).toBeTruthy(); + expect(existsSync(`${buildPath}/dep1`)).toBeTruthy(); + expect(existsSync(`${buildPath}/dist/app.fake`)).toBeTruthy(); + expect(spawn.sync).toHaveBeenCalledWith('poetry', ['build'], { + cwd: buildPath, + shell: false, + stdio: 'inherit', + }); + + const projectTomlData = parse( + readFileSync(`${buildPath}/pyproject.toml`).toString('utf-8'), + ) as PyprojectToml; + + expect(projectTomlData.tool.poetry.packages).toStrictEqual([ + { + include: 'app', + }, + { + include: 'dep1', + }, + ]); + + expect(projectTomlData.tool.poetry.dependencies).toStrictEqual({ + python: '^3.8', + click: '7.1.2', + numpy: { + version: '1.21.0', + optional: false, + markers: 'python_version >= "3.8" and python_version < "4.0"', + }, + }); + expect(projectTomlData.tool.poetry.group.dev.dependencies).toStrictEqual( + {}, + ); + + expect(output.success).toBe(true); + }); + + it('should throw an exception when poetry-plugin-export@1.8.0 local project is not a valid poetry project', async () => { + fsMock({ + 'apps/app/.venv/pyvenv.cfg': 'fake', + 'apps/app/app/index.py': 'print("Hello from app")', + 'apps/app/poetry.lock': dedent` + [[package]] + name = "click" + version = "7.1.2" + description = "Composable command line interface toolkit" + category = "main" + optional = false + python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + + [[package]] + name = "dep1" + version = "1.0.0" + description = "Dep1" + category = "main" + optional = false + python-versions = "^3.8" + develop = false + + [package.dependencies] + numpy = "1.21.0" + + [package.source] + type = "directory" + url = "../../libs/dep1" + + [[package]] + name = "numpy" + version = "1.21.0" + description = "NumPy is the fundamental package for array computing with Python." + category = "main" + optional = false + python-versions = ">=3.7" + `, + + 'apps/app/pyproject.toml': dedent` + [tool.poetry] + name = "app" + version = "1.0.0" + [[tool.poetry.packages]] + include = "app" + + [tool.poetry.dependencies] + python = "^3.8" + click = "7.1.2" + dep1 = { path = "../../libs/dep1" } + + [tool.poetry.group.dev.dependencies] + pytest = "6.2.4" + `, + + 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', + 'libs/dep1/pyproject.toml': dedent` + [tool.poetry] + name = "dep1" + version = "1.0.0" + [[tool.poetry.packages]] + include = "dep1" + + [tool.poetry.dependencies] + python = "^3.8" + numpy = "1.21.0" + + [tool.poetry.group.dev.dependencies] + pytest = "6.2.4" + `, + + 'libs/dep2/dep2/index.py': 'print("Hello from dep2")', + 'libs/dep2/pyproject.toml': dedent` + [tool.poetry] + name = "dep2" + version = "1.0.0" + [[tool.poetry.packages]] + include = "dep2" + + [tool.poetry.dependencies] + python = "^3.8" + + [tool.poetry.group.dev.dependencies] + pytest = "6.2.4" + `, + }); + + vi.mocked(spawn.sync).mockImplementation((_, args, opts) => { + if (args[0] == 'build') { + spawnBuildMockImpl(opts); + } else if (args[0] == 'export' && opts.cwd === 'apps/app') { + writeFileSync( + join(buildPath, 'requirements.txt'), + dedent` + click==7.1.2 + -e file://${process.cwd()}/libs/dep10 + numpy==1.21.0; python_version >= "3.8" and python_version < "4.0" + + `, + ); + } + return { + status: 0, + output: [''], + pid: 0, + signal: null, + stderr: null, + stdout: null, + }; + }); + + const options: BuildExecutorSchema = { + ignorePaths: ['.venv', '.tox', 'tests/'], + silent: false, + outputPath: 'dist/apps/app', + keepBuildFolder: true, + devDependencies: false, + lockedVersions: true, + bundleLocalDependencies: true, + }; + + const output = await executor(options, { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + workspace: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + dep1: { + root: 'libs/dep1', + targets: {}, + }, + dep2: { + root: 'libs/dep2', + targets: {}, + }, + }, + }, + }); + + expect(output.success).toBe(false); + }); + it('should build python project with git dependency with revision and markers', async () => { fsMock({ 'apps/app/.venv/pyvenv.cfg': 'fake', diff --git a/packages/nx-python/src/executors/build/resolvers/locked.ts b/packages/nx-python/src/executors/build/resolvers/locked.ts index c1e2839..6ad2787 100644 --- a/packages/nx-python/src/executors/build/resolvers/locked.ts +++ b/packages/nx-python/src/executors/build/resolvers/locked.ts @@ -1,11 +1,11 @@ import { PyprojectToml } from '../../../graph/dependency-graph'; import { parse } from '@iarna/toml'; -import { readFileSync } from 'fs-extra'; -import { join, relative } from 'path'; +import { readFileSync, existsSync } from 'fs-extra'; +import path, { join, relative } from 'path'; import { PoetryLock, Dependency, PoetryLockPackage } from './types'; import { Logger } from '../../utils/logger'; import chalk from 'chalk'; -import { runPoetry } from '../../utils/poetry'; +import { parseToml, runPoetry } from '../../utils/poetry'; import uri2path from 'file-uri-to-path'; import { getLoggingTab, includeDependencyPackage } from './utils'; @@ -64,7 +64,7 @@ export class LockedDependencyResolver { dep.markers = elements[1].trim(); } - if (elements[0].includes('@')) { + if (elements[0].includes('@') || elements[0].startsWith('-e file:')) { this.resolveSourceDependency( tab, elements, @@ -158,9 +158,8 @@ export class LockedDependencyResolver { buildTomlData: PyprojectToml, deps: Dependency[], ) { - const atPosition = exportedLineElements[0].indexOf('@'); - const packageName = exportedLineElements[0].substring(0, atPosition).trim(); - const location = exportedLineElements[0].substring(atPosition + 1).trim(); + const { packageName, location } = + this.extractLocalPackageInfo(exportedLineElements); dep.name = packageName; this.resolvePackageExtras(dep); @@ -186,6 +185,29 @@ export class LockedDependencyResolver { } } + private extractLocalPackageInfo(exportedLineElements: string[]) { + if (exportedLineElements[0].startsWith('-e file:')) { + const location = exportedLineElements[0].substring(10).trim(); + const pyprojectToml = path.join(location, 'pyproject.toml'); + if (!existsSync(pyprojectToml)) { + throw new Error( + chalk`pyproject.toml not found in {blue.bold ${location}}`, + ); + } + + const pyproject = parseToml(pyprojectToml); + return { + packageName: pyproject.tool.poetry.name, + location: `file://${location}`, + }; + } + + const atPosition = exportedLineElements[0].indexOf('@'); + const packageName = exportedLineElements[0].substring(0, atPosition).trim(); + const location = exportedLineElements[0].substring(atPosition + 1).trim(); + return { packageName, location }; + } + private resolvePackageExtras(dep: Dependency) { if (dep.name.indexOf('[') !== -1) { dep.extras = dep.name diff --git a/packages/nx-python/src/executors/remove/executor.spec.ts b/packages/nx-python/src/executors/remove/executor.spec.ts index 6f47ec5..9c41b68 100644 --- a/packages/nx-python/src/executors/remove/executor.spec.ts +++ b/packages/nx-python/src/executors/remove/executor.spec.ts @@ -505,10 +505,15 @@ version = "1.0.0" stdio: 'inherit', }, ); - expect(spawn.sync).toHaveBeenNthCalledWith(3, 'poetry', ['install'], { - shell: false, - stdio: 'inherit', - }); + expect(spawn.sync).toHaveBeenNthCalledWith( + 3, + 'poetry', + ['install', '--no-root'], + { + shell: false, + stdio: 'inherit', + }, + ); expect(output.success).toBe(true); }); @@ -582,10 +587,15 @@ version = "1.0.0" stdio: 'inherit', }, ); - expect(spawn.sync).toHaveBeenNthCalledWith(3, 'poetry', ['install'], { - shell: false, - stdio: 'inherit', - }); + expect(spawn.sync).toHaveBeenNthCalledWith( + 3, + 'poetry', + ['install', '--no-root'], + { + shell: false, + stdio: 'inherit', + }, + ); expect(output.success).toBe(true); }); }); diff --git a/packages/nx-python/src/executors/update/executor.spec.ts b/packages/nx-python/src/executors/update/executor.spec.ts index 378d545..c4558c3 100644 --- a/packages/nx-python/src/executors/update/executor.spec.ts +++ b/packages/nx-python/src/executors/update/executor.spec.ts @@ -657,10 +657,15 @@ version = "1.0.0" stdio: 'inherit', }, ); - expect(spawn.sync).toHaveBeenNthCalledWith(3, 'poetry', ['install'], { - shell: false, - stdio: 'inherit', - }); + expect(spawn.sync).toHaveBeenNthCalledWith( + 3, + 'poetry', + ['install', '--no-root'], + { + shell: false, + stdio: 'inherit', + }, + ); expect(output.success).toBe(true); }); @@ -816,10 +821,15 @@ version = "1.0.0" stdio: 'inherit', }, ); - expect(spawn.sync).toHaveBeenNthCalledWith(6, 'poetry', ['install'], { - shell: false, - stdio: 'inherit', - }); + expect(spawn.sync).toHaveBeenNthCalledWith( + 6, + 'poetry', + ['install', '--no-root'], + { + shell: false, + stdio: 'inherit', + }, + ); expect(output.success).toBe(true); }); });