From 6e1a634b64b666d2bbe6fcfcc8dc6be876b011fb Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Wed, 18 Dec 2024 13:08:46 -0300 Subject: [PATCH] feat(nx-python): change `uv` to support non-workspace projects (#260) --- packages/nx-python/README.md | 118 +- packages/nx-python/package.json | 3 +- .../src/executors/add/executor.spec.ts | 581 ++-- .../nx-python/src/executors/add/executor.ts | 7 +- .../src/executors/build/executor.spec.ts | 2424 +++++++++++------ .../nx-python/src/executors/build/executor.ts | 7 +- .../src/executors/install/executor.spec.ts | 287 +- .../src/executors/install/executor.ts | 7 +- .../src/executors/remove/executor.spec.ts | 285 +- .../src/executors/remove/executor.ts | 7 +- .../executors/run-commands/executor.spec.ts | 71 +- .../src/executors/run-commands/executor.ts | 9 +- .../src/executors/update/executor.spec.ts | 296 +- .../src/executors/update/executor.ts | 7 +- .../__snapshots__/generator.spec.ts.snap | 129 +- .../{ => base}/__dot__python-version.template | 0 .../files/{ => poetry}/poetry.toml | 0 .../files/{ => poetry}/pyproject.toml | 0 .../files/uv/pyproject.toml | 7 + .../migrate-to-shared-venv/generator.spec.ts | 377 ++- .../migrate-to-shared-venv/generator.ts | 146 +- .../migrate-to-shared-venv/schema.d.ts | 1 + .../migrate-to-shared-venv/schema.json | 6 + .../__snapshots__/generator.spec.ts.snap | 1212 +++++---- .../uv-project/files/base/pyproject.toml | 37 +- .../generators/uv-project/generator.spec.ts | 784 +++--- .../src/generators/uv-project/generator.ts | 70 +- packages/nx-python/src/provider/base.ts | 2 +- .../nx-python/src/provider/poetry/provider.ts | 2 +- .../nx-python/src/provider/poetry/utils.ts | 14 - packages/nx-python/src/provider/resolver.ts | 66 +- packages/nx-python/src/provider/utils.ts | 17 +- .../src/provider/uv/build/resolvers/locked.ts | 22 +- .../provider/uv/build/resolvers/project.ts | 51 +- .../nx-python/src/provider/uv/provider.ts | 207 +- packages/nx-python/src/provider/uv/types.ts | 4 + packages/nx-python/src/provider/uv/utils.ts | 13 +- 37 files changed, 4741 insertions(+), 2535 deletions(-) rename packages/nx-python/src/generators/migrate-to-shared-venv/files/{ => base}/__dot__python-version.template (100%) rename packages/nx-python/src/generators/migrate-to-shared-venv/files/{ => poetry}/poetry.toml (100%) rename packages/nx-python/src/generators/migrate-to-shared-venv/files/{ => poetry}/pyproject.toml (100%) create mode 100644 packages/nx-python/src/generators/migrate-to-shared-venv/files/uv/pyproject.toml diff --git a/packages/nx-python/README.md b/packages/nx-python/README.md index 49f4cff..f7252c9 100644 --- a/packages/nx-python/README.md +++ b/packages/nx-python/README.md @@ -125,42 +125,6 @@ Nx documentation reference: The projects still have their own `pyproject.toml` file to manage each project's dependencies, however, the package versions cannot conflict because the root `pyproject.toml` file is referencing all the dependencies. - -**Benefits**: - -- Save time in the local environment and CI tool -- Reduce the size of the workspace -- Reduce the number of dependencies installed in the local environment and CI tool -- Single-version policy (recommended by Nx) -- Better VSCode integration (currently, the VSCode Python extension doesn't support multiple virtual environments in the same workspace, it needs to switch between them manually) - -**Cons**: - -- Package versions cannot conflict at the workspace level -- Local packages with the same module name don't work properly in the VSCode, because when the VSCode Python extension is activated, it uses the root `pyproject.toml` file to resolve the packages, so, it will use the first module found in the `pyproject.toml` file. - ##### devDependenciesProject This approach consists of moving all the dev dependencies from the projects to separate projects, this project is referenced in the root `pyproject.toml` and all the local projects as a dev dependency. @@ -254,9 +218,42 @@ nx generate @nxlv/python:uv-project myproject | `--codeCoverageThreshold` | `number` | Minimum Code Coverage Threshold | `false` | N/A | | `--projectNameAndRootFormat` | `string` | Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`). | `false` | `as-provided` | -Because the Uv package manager has native support for workspaces, the `@nxlv/python` plugin enforces the use of a shared virtual environment by default, so, all the projects in the workspace are using the same virtual environment. +### Shared Virtual Environment -**IMPORTANT**: The `@nxlv/python:migrate-to-shared-venv` generator is not available for Uv projects, since it's already enforced by default. +By default, the `@nxlv/python` manages the projects individually, so, all the projects have their one set of dependencies and virtual environments. + +However, In some cases, we want to use a shared virtual environment for the entire workspace to save some installation time in your local environment and CI tool, we use this mode when the workspace contains many projects with the same dependencies and versions that don't conflict in the workspace level. + +To migrate to this mode, run the following command: + +```bash +npx nx generate @nxlv/python:migrate-to-shared-venv +``` + +**Options**: + +| Option | Type | Description | Required | Default | +| ----------------------- | :-------: | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------- | +| `--moveDevDependencies` | `boolean` | Specifies if migration moves the dev dependencies from the projects to the root `pyproject.toml` | `true` | `true` | +| `--autoActivate` | `boolean` | Adds the `autoActivate` config in the root `pyproject.toml`, this flag is used to auto-activate the venv when the `@nxlv/python` executors are called | `true` | `true` | +| `--packageManager` | `string` | Specifies the package manager to be used in the root `pyproject.toml` (`poetry` or `uv`) | `true` | `poetry` | + +After the migration is completed, the workspace now has a `pyproject.toml` in the root directory, and all the local projects are referencing the root `pyproject.toml` file. + +> The projects still have their own `pyproject.toml` file to manage each project's dependencies, however, the package versions cannot conflict because the root `pyproject.toml` file is referencing all the dependencies. + +**Benefits**: + +- Save time in the local environment and CI tool +- Reduce the size of the workspace +- Reduce the number of dependencies installed in the local environment and CI tool +- Single-version policy (recommended by Nx) +- Better VSCode integration (currently, the VSCode Python extension doesn't support multiple virtual environments in the same workspace, it needs to switch between them manually) + +**Cons**: + +- Package versions cannot conflict at the workspace level +- Local packages with the same module name don't work properly in the VSCode, because when the VSCode Python extension is activated, it uses the root `pyproject.toml` file to resolve the packages, so, it will use the first module found in the `pyproject.toml` file. ### Executors @@ -265,7 +262,7 @@ Because the Uv package manager has native support for workspaces, the `@nxlv/pyt The `@nxlv/python:add` executor handles `add` command to provide a level of abstraction and control in the monorepo projects. - `poetry`: `poetry add {args}` -- `uv`: `uv add {args} --project {projectPath}` +- `uv`: `uv add {args}` ##### Features @@ -276,11 +273,11 @@ The `@nxlv/python:add` executor handles `add` command to provide a level of abst ##### Options -| Option | Type | Description | Required | Default | -| --------- | :-------: | ------------------------------------------------------------------- | ---------------------------------------------------- | ------- | -| `--name` | `string` | Dependency name (if local dependency use the Nx project name) | `true` | | -| `--args` | `string` | Custom args to be used in the `add` command | `false` | | -| `--local` | `boolean` | Specifies if the dependency is local, not necessary for Uv projects | `false` (only if the `--name` is a local dependency) | | +| Option | Type | Description | Required | Default | +| --------- | :-------: | ------------------------------------------------------------- | ---------------------------------------------------- | ------- | +| `--name` | `string` | Dependency name (if local dependency use the Nx project name) | `true` | | +| `--args` | `string` | Custom args to be used in the `add` command | `false` | | +| `--local` | `boolean` | Specifies if the dependency is local | `false` (only if the `--name` is a local dependency) | | #### update @@ -288,30 +285,30 @@ The `@nxlv/python:update` executor handles `update` command to provide a level o - `poetry`: `poetry update {args}` - `uv`: Uv doesn't have a native update command, so, the executor runs the following commands: - - `uv lock --upgrade-package {name} --project {projectPath}` + - `uv lock --upgrade-package {name}` - `uv sync` ##### Features - Update external dependencies -- Update local dependencies (Poetry only) +- Update local dependencies > Both features updates the local workspace dependency tree to keep the lock/venv updated. ##### Options -| Option | Type | Description | Required | Default | -| --------- | :-------: | ------------------------------------------------------------------- | ---------------------------------------------------- | ------- | -| `--name` | `string` | Dependency name (if local dependency use the Nx project name) | `false` | | -| `--args` | `string` | Custom args to be used in the `update` command | `false` | | -| `--local` | `boolean` | Specifies if the dependency is local, not necessary for Uv projects | `false` (only if the `--name` is a local dependency) | | +| Option | Type | Description | Required | Default | +| --------- | :-------: | ------------------------------------------------------------- | ---------------------------------------------------- | ------- | +| `--name` | `string` | Dependency name (if local dependency use the Nx project name) | `false` | | +| `--args` | `string` | Custom args to be used in the `update` command | `false` | | +| `--local` | `boolean` | Specifies if the dependency is local | `false` (only if the `--name` is a local dependency) | | #### remove The `@nxlv/python:remove` executor handles `remove` command to provide a level of abstraction and control in the monorepo projects. - `poetry`: `poetry remove {args}` -- `uv`: `uv remove {args} --project {projectPath}` +- `uv`: `uv remove {args}` ##### Features @@ -322,11 +319,11 @@ The `@nxlv/python:remove` executor handles `remove` command to provide a level o ##### Options -| Option | Type | Description | Required | Default | -| --------- | :-------: | ------------------------------------------------------------------- | ---------------------------------------------------- | ------- | -| `--name` | `string` | Dependency name (if local dependency use the Nx project name) | `true` | | -| `--args` | `string` | Custom args to be used in the `remove` command | `false` | | -| `--local` | `boolean` | Specifies if the dependency is local, not necessary for Uv projects | `false` (only if the `--name` is a local dependency) | | +| Option | Type | Description | Required | Default | +| --------- | :-------: | ------------------------------------------------------------- | ---------------------------------------------------- | ------- | +| `--name` | `string` | Dependency name (if local dependency use the Nx project name) | `true` | | +| `--args` | `string` | Custom args to be used in the `remove` command | `false` | | +| `--local` | `boolean` | Specifies if the dependency is local | `false` (only if the `--name` is a local dependency) | | #### build @@ -517,9 +514,12 @@ The `@nxlv/python:flake8` handles the `flake8` linting tasks and reporting gener | `--silent` | `boolean` | Hide output text | `false` | `false` | | `--outputFile` | `string` | Output pylint file path | `true` | | -#### install (poetry only) +#### install + +The `@nxlv/python:install` handles the `install` command for a project. -The `@nxlv/python:install` handles the `poetry install` command for a project. +- `poetry`: `poetry install {args}` +- `uv`: `uv install {args}` ##### Options diff --git a/packages/nx-python/package.json b/packages/nx-python/package.json index cd71e4f..8674482 100644 --- a/packages/nx-python/package.json +++ b/packages/nx-python/package.json @@ -23,8 +23,7 @@ "lodash": "^4.17.21", "@nx/devkit": "^20.0.0", "ora": "5.3.0", - "semver": "^7.5.3", - "wildcard-match": "^5.1.3" + "semver": "^7.5.3" }, "nx-migrations": { "migrations": "./migrations.json" diff --git a/packages/nx-python/src/executors/add/executor.spec.ts b/packages/nx-python/src/executors/add/executor.spec.ts index 3277c52..be976e0 100644 --- a/packages/nx-python/src/executors/add/executor.spec.ts +++ b/packages/nx-python/src/executors/add/executor.spec.ts @@ -1323,215 +1323,426 @@ describe('Add Executor', () => { vi.spyOn(process, 'chdir').mockReturnValue(undefined); }); - beforeEach(() => { - vol.fromJSON({ - 'uv.lock': '', + describe('workspace', () => { + beforeEach(() => { + vol.fromJSON({ + 'uv.lock': '', + }); }); - }); - - it('should return success false when the uv is not installed', async () => { - checkPrerequisites.mockRejectedValue(new Error('uv not found')); - const options = { - name: 'numpy', - local: false, - }; - - const context: ExecutorContext = { - cwd: '', - root: '.', - isVerbose: false, - projectName: 'app', - projectsConfigurations: { - version: 2, - projects: { - app: { - root: 'apps/app', - targets: {}, + it('should return success false when the uv is not installed', async () => { + checkPrerequisites.mockRejectedValue(new Error('uv not found')); + + const options = { + name: 'numpy', + local: false, + }; + + const context: ExecutorContext = { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, }, }, - }, - nxJsonConfiguration: {}, - projectGraph: { - dependencies: {}, - nodes: {}, - }, - }; - - const output = await executor(options, context); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(spawn.sync).not.toHaveBeenCalled(); - expect(output.success).toBe(false); - }); + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }; - it('run add target and should add the dependency to the project', async () => { - const options = { - name: 'numpy', - local: false, - }; + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).not.toHaveBeenCalled(); + expect(output.success).toBe(false); + }); - const context: ExecutorContext = { - cwd: '', - root: '.', - isVerbose: false, - projectName: 'app', - projectsConfigurations: { - version: 2, - projects: { - app: { - root: 'apps/app', - targets: {}, + it('run add target and should add the dependency to the project', async () => { + const options = { + name: 'numpy', + local: false, + }; + + const context: ExecutorContext = { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, }, }, - }, - nxJsonConfiguration: {}, - projectGraph: { - dependencies: {}, - nodes: {}, - }, - }; - - const output = await executor(options, context); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(spawn.sync).toHaveBeenCalledWith( - 'uv', - ['add', 'numpy', '--project', 'apps/app'], - { - cwd: '.', - shell: false, - stdio: 'inherit', - }, - ); - expect(output.success).toBe(true); - }); + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }; + + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).toHaveBeenCalledWith( + 'uv', + ['add', 'numpy', '--project', 'apps/app'], + { + cwd: '.', + shell: false, + stdio: 'inherit', + }, + ); + expect(output.success).toBe(true); + }); - it('run add target and should add the dependency to the project group dev', async () => { - const options = { - name: 'numpy', - local: false, - group: 'dev', - }; + it('run add target and should add the dependency to the project group dev', async () => { + const options = { + name: 'numpy', + local: false, + group: 'dev', + }; + + const context: ExecutorContext = { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + }, + }, + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }; + + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).toHaveBeenCalledWith( + 'uv', + ['add', 'numpy', '--group', 'dev', '--project', 'apps/app'], + { + cwd: '.', + shell: false, + stdio: 'inherit', + }, + ); + expect(output.success).toBe(true); + }); - const context: ExecutorContext = { - cwd: '', - root: '.', - isVerbose: false, - projectName: 'app', - projectsConfigurations: { - version: 2, - projects: { - app: { - root: 'apps/app', - targets: {}, + it('run add target and should add the dependency to the project extras', async () => { + const options = { + name: 'numpy', + local: false, + extras: ['dev'], + }; + + const context: ExecutorContext = { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, }, }, - }, - nxJsonConfiguration: {}, - projectGraph: { - dependencies: {}, - nodes: {}, - }, - }; + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }; + + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).toHaveBeenCalledWith( + 'uv', + ['add', 'numpy', '--extra', 'dev', '--project', 'apps/app'], + { + cwd: '.', + shell: false, + stdio: 'inherit', + }, + ); + expect(output.success).toBe(true); + }); - const output = await executor(options, context); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(spawn.sync).toHaveBeenCalledWith( - 'uv', - ['add', 'numpy', '--project', 'apps/app', '--group', 'dev'], - { - cwd: '.', - shell: false, - stdio: 'inherit', - }, - ); - expect(output.success).toBe(true); + it('run add target and should throw an exception', async () => { + vi.mocked(spawn.sync).mockImplementation(() => { + throw new Error('fake error'); + }); + + const options = { + name: 'numpy', + local: false, + }; + + const context: ExecutorContext = { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + }, + }, + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }; + + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).toHaveBeenCalledWith( + 'uv', + ['add', 'numpy', '--project', 'apps/app'], + { + cwd: '.', + shell: false, + stdio: 'inherit', + }, + ); + expect(output.success).toBe(false); + }); }); - it('run add target and should add the dependency to the project extras', async () => { - const options = { - name: 'numpy', - local: false, - extras: ['dev'], - }; - - const context: ExecutorContext = { - cwd: '', - root: '.', - isVerbose: false, - projectName: 'app', - projectsConfigurations: { - version: 2, - projects: { - app: { - root: 'apps/app', - targets: {}, + describe('project', () => { + it('run add target and should update all the dependency tree', async () => { + vol.fromJSON({ + 'apps/app/pyproject.toml': dedent` + [project] + name = "app" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "lib1", + ] + + [tool.hatch.build.targets.wheel] + packages = ["app"] + + [tool.uv.sources] + lib1 = { path = "../../libs/lib1" } + `, + + 'apps/app1/pyproject.toml': dedent` + [project] + name = "app1" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "lib1", + ] + + [tool.hatch.build.targets.wheel] + packages = ["app1"] + + [tool.uv.sources] + lib1 = { path = "../../libs/lib1" } + `, + + 'libs/lib1/pyproject.toml': dedent` + [project] + name = "lib1" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "shared1", + ] + + [tool.hatch.build.targets.wheel] + packages = ["lib1"] + + [tool.uv.sources] + shared1 = { path = "../shared1" } + `, + + 'libs/shared1/pyproject.toml': dedent` + [project] + name = "shared1" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + + [tool.hatch.build.targets.wheel] + packages = ["shared1"] + `, + }); + + const options = { + name: 'numpy', + local: false, + }; + + const context: ExecutorContext = { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'shared1', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + app1: { + root: 'apps/app1', + targets: {}, + }, + app3: { + root: 'apps/app3', + targets: {}, + }, + lib1: { + root: 'libs/lib1', + targets: {}, + }, + shared1: { + root: 'libs/shared1', + targets: {}, + }, }, }, - }, - nxJsonConfiguration: {}, - projectGraph: { - dependencies: {}, - nodes: {}, - }, - }; + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }; - const output = await executor(options, context); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(spawn.sync).toHaveBeenCalledWith( - 'uv', - ['add', 'numpy', '--project', 'apps/app', '--extra', 'dev'], - { - cwd: '.', + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).toHaveBeenCalledTimes(4); + expect(spawn.sync).toHaveBeenNthCalledWith(1, 'uv', ['add', 'numpy'], { + cwd: 'libs/shared1', shell: false, stdio: 'inherit', - }, - ); - expect(output.success).toBe(true); - }); - - it('run add target and should throw an exception', async () => { - vi.mocked(spawn.sync).mockImplementation(() => { - throw new Error('fake error'); + }); + expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['sync'], { + cwd: 'libs/lib1', + shell: false, + stdio: 'inherit', + }); + expect(spawn.sync).toHaveBeenNthCalledWith(3, 'uv', ['sync'], { + cwd: 'apps/app', + shell: false, + stdio: 'inherit', + }); + expect(spawn.sync).toHaveBeenNthCalledWith(4, 'uv', ['sync'], { + cwd: 'apps/app1', + shell: false, + stdio: 'inherit', + }); + expect(output.success).toBe(true); }); - const options = { - name: 'numpy', - local: false, - }; - - const context: ExecutorContext = { - cwd: '', - root: '.', - isVerbose: false, - projectName: 'app', - projectsConfigurations: { - version: 2, - projects: { - app: { - root: 'apps/app', - targets: {}, + it('run add target with local dependency', async () => { + vol.fromJSON({ + 'apps/app/pyproject.toml': dedent` + [project] + name = "app" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + + [tool.hatch.build.targets.wheel] + packages = ["app"] + `, + + 'libs/lib1/pyproject.toml': dedent` + [project] + name = "lib1" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + + [tool.hatch.build.targets.wheel] + packages = ["lib1"] + `, + }); + + const options = { + name: 'lib1', + local: true, + }; + + const context: ExecutorContext = { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + lib1: { + root: 'libs/lib1', + targets: {}, + }, }, }, - }, - nxJsonConfiguration: {}, - projectGraph: { - dependencies: {}, - nodes: {}, - }, - }; - - const output = await executor(options, context); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(spawn.sync).toHaveBeenCalledWith( - 'uv', - ['add', 'numpy', '--project', 'apps/app'], - { - cwd: '.', - shell: false, - stdio: 'inherit', - }, - ); - expect(output.success).toBe(false); + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }; + + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).toHaveBeenCalledTimes(1); + expect(spawn.sync).toHaveBeenNthCalledWith( + 1, + 'uv', + ['add', '--editable', '../../libs/lib1'], + { + cwd: 'apps/app', + shell: false, + stdio: 'inherit', + }, + ); + expect(output.success).toBe(true); + }); }); }); }); diff --git a/packages/nx-python/src/executors/add/executor.ts b/packages/nx-python/src/executors/add/executor.ts index 48ddd1b..faf62ef 100644 --- a/packages/nx-python/src/executors/add/executor.ts +++ b/packages/nx-python/src/executors/add/executor.ts @@ -10,7 +10,12 @@ export default async function executor( const workspaceRoot = context.root; process.chdir(workspaceRoot); try { - const provider = await getProvider(workspaceRoot); + const provider = await getProvider( + workspaceRoot, + undefined, + undefined, + context, + ); await provider.add(options, context); return { success: true, diff --git a/packages/nx-python/src/executors/build/executor.spec.ts b/packages/nx-python/src/executors/build/executor.spec.ts index 1d38432..9cc1afe 100644 --- a/packages/nx-python/src/executors/build/executor.spec.ts +++ b/packages/nx-python/src/executors/build/executor.spec.ts @@ -3075,11 +3075,12 @@ describe('Build Executor', () => { }); describe('locked resolver', () => { - it('should skip the local project dependency if the pyproject is not found', async () => { - vol.fromJSON({ - 'apps/app/app1/index.py': 'print("Hello from app")', + describe('workspaces', () => { + it('should skip the local project dependency if the pyproject is not found', async () => { + vol.fromJSON({ + 'apps/app/app1/index.py': 'print("Hello from app")', - 'apps/app/pyproject.toml': dedent` + 'apps/app/pyproject.toml': dedent` [project] name = "app1" version = "0.1.0" @@ -3101,122 +3102,122 @@ describe('Build Executor', () => { [tool.uv.sources] dep1 = { workspace = true } `, - }); + }); - vi.mocked(spawn.sync) - .mockReturnValueOnce({ - status: 0, - output: [''], - pid: 0, - signal: null, - stderr: null, - stdout: Buffer.from(dedent` - -e ./libs/dep1 - django==5.1.4 - `), - }) - .mockImplementationOnce((_, args, opts) => { - spawnBuildMockImpl(opts); - return { + vi.mocked(spawn.sync) + .mockReturnValueOnce({ 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, - }; + stdout: Buffer.from(dedent` + -e ./libs/dep1 + django==5.1.4 + `), + }) + .mockImplementationOnce((_, args, opts) => { + spawnBuildMockImpl(opts); + 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', - projectsConfigurations: { - version: 2, - projects: { - app: { - root: 'apps/app', - targets: {}, - }, - dep1: { - root: 'libs/dep1', - targets: {}, + const output = await executor(options, { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + dep1: { + root: 'libs/dep1', + targets: {}, + }, }, }, - }, - nxJsonConfiguration: {}, - projectGraph: { - dependencies: {}, - nodes: {}, - }, - }); + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(existsSync(buildPath)).toBeTruthy(); - expect(existsSync(`${buildPath}/app1`)).toBeTruthy(); - expect(existsSync(`${buildPath}/dep1`)).not.toBeTruthy(); - expect(existsSync(`${buildPath}/dist/app.fake`)).toBeTruthy(); - expect(spawn.sync).toHaveBeenCalledTimes(2); - expect(spawn.sync).toHaveBeenNthCalledWith( - 1, - 'uv', - [ - 'export', - '--format', - 'requirements-txt', - '--no-hashes', - '--no-header', - '--frozen', - '--no-emit-project', - '--all-extras', - '--project', - 'apps/app', - '--no-dev', - ], - { - cwd: '.', - shell: true, - stdio: 'pipe', - }, - ); - expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['build'], { - cwd: buildPath, - shell: false, - stdio: 'inherit', - }); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(existsSync(buildPath)).toBeTruthy(); + expect(existsSync(`${buildPath}/app1`)).toBeTruthy(); + expect(existsSync(`${buildPath}/dep1`)).not.toBeTruthy(); + expect(existsSync(`${buildPath}/dist/app.fake`)).toBeTruthy(); + expect(spawn.sync).toHaveBeenCalledTimes(2); + expect(spawn.sync).toHaveBeenNthCalledWith( + 1, + 'uv', + [ + 'export', + '--format', + 'requirements-txt', + '--no-hashes', + '--no-header', + '--frozen', + '--no-emit-project', + '--all-extras', + '--project', + 'apps/app', + '--no-dev', + ], + { + cwd: '.', + shell: true, + stdio: 'pipe', + }, + ); + expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['build'], { + cwd: buildPath, + shell: false, + stdio: 'inherit', + }); - const projectTomlData = getPyprojectData( - `${buildPath}/pyproject.toml`, - ); + const projectTomlData = getPyprojectData( + `${buildPath}/pyproject.toml`, + ); - expect( - projectTomlData.tool.hatch.build.targets.wheel.packages, - ).toStrictEqual(['app1']); + expect( + projectTomlData.tool.hatch.build.targets.wheel.packages, + ).toStrictEqual(['app1']); - expect(projectTomlData.project.dependencies).toStrictEqual([ - 'django==5.1.4', - ]); - expect(projectTomlData['dependency-groups']).toStrictEqual({}); + expect(projectTomlData.project.dependencies).toStrictEqual([ + 'django==5.1.4', + ]); + expect(projectTomlData['dependency-groups']).toStrictEqual({}); - expect(output.success).toBe(true); - }); + expect(output.success).toBe(true); + }); - it('should build python project with local dependencies', async () => { - vol.fromJSON({ - 'apps/app/app1/index.py': 'print("Hello from app")', + it('should build python project with local dependencies', async () => { + vol.fromJSON({ + 'apps/app/app1/index.py': 'print("Hello from app")', - 'apps/app/pyproject.toml': dedent` + 'apps/app/pyproject.toml': dedent` [project] name = "app1" version = "0.1.0" @@ -3239,8 +3240,8 @@ describe('Build Executor', () => { dep1 = { workspace = true } `, - 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', - 'libs/dep1/pyproject.toml': dedent` + 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', + 'libs/dep1/pyproject.toml': dedent` [project] name = "dep1" version = "0.1.0" @@ -3251,123 +3252,123 @@ describe('Build Executor', () => { [tool.hatch.build.targets.wheel] packages = ["dep1"] `, - }); + }); - vi.mocked(spawn.sync) - .mockReturnValueOnce({ - status: 0, - output: [''], - pid: 0, - signal: null, - stderr: null, - stdout: Buffer.from(dedent` - -e ./libs/dep1 - django==5.1.4 - `), - }) - .mockImplementationOnce((_, args, opts) => { - spawnBuildMockImpl(opts); - return { + vi.mocked(spawn.sync) + .mockReturnValueOnce({ 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, - }; + stdout: Buffer.from(dedent` + -e ./libs/dep1 + django==5.1.4 + `), + }) + .mockImplementationOnce((_, args, opts) => { + spawnBuildMockImpl(opts); + 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', - projectsConfigurations: { - version: 2, - projects: { - app: { - root: 'apps/app', - targets: {}, - }, - dep1: { - root: 'libs/dep1', - targets: {}, + const output = await executor(options, { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + dep1: { + root: 'libs/dep1', + targets: {}, + }, }, }, - }, - nxJsonConfiguration: {}, - projectGraph: { - dependencies: {}, - nodes: {}, - }, - }); + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }); - expect(checkPrerequisites).toHaveBeenCalled(); - console.log('buildPath', buildPath); - expect(existsSync(buildPath)).toBeTruthy(); - expect(existsSync(`${buildPath}/app1`)).toBeTruthy(); - expect(existsSync(`${buildPath}/dep1`)).toBeTruthy(); - expect(existsSync(`${buildPath}/dist/app.fake`)).toBeTruthy(); - expect(spawn.sync).toHaveBeenCalledTimes(2); - expect(spawn.sync).toHaveBeenNthCalledWith( - 1, - 'uv', - [ - 'export', - '--format', - 'requirements-txt', - '--no-hashes', - '--no-header', - '--frozen', - '--no-emit-project', - '--all-extras', - '--project', - 'apps/app', - '--no-dev', - ], - { - cwd: '.', - shell: true, - stdio: 'pipe', - }, - ); - expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['build'], { - cwd: buildPath, - shell: false, - stdio: 'inherit', - }); + expect(checkPrerequisites).toHaveBeenCalled(); + console.log('buildPath', buildPath); + expect(existsSync(buildPath)).toBeTruthy(); + expect(existsSync(`${buildPath}/app1`)).toBeTruthy(); + expect(existsSync(`${buildPath}/dep1`)).toBeTruthy(); + expect(existsSync(`${buildPath}/dist/app.fake`)).toBeTruthy(); + expect(spawn.sync).toHaveBeenCalledTimes(2); + expect(spawn.sync).toHaveBeenNthCalledWith( + 1, + 'uv', + [ + 'export', + '--format', + 'requirements-txt', + '--no-hashes', + '--no-header', + '--frozen', + '--no-emit-project', + '--all-extras', + '--project', + 'apps/app', + '--no-dev', + ], + { + cwd: '.', + shell: true, + stdio: 'pipe', + }, + ); + expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['build'], { + cwd: buildPath, + shell: false, + stdio: 'inherit', + }); - const projectTomlData = getPyprojectData( - `${buildPath}/pyproject.toml`, - ); + const projectTomlData = getPyprojectData( + `${buildPath}/pyproject.toml`, + ); - expect( - projectTomlData.tool.hatch.build.targets.wheel.packages, - ).toStrictEqual(['app1', 'dep1']); + expect( + projectTomlData.tool.hatch.build.targets.wheel.packages, + ).toStrictEqual(['app1', 'dep1']); - expect(projectTomlData.project.dependencies).toStrictEqual([ - 'django==5.1.4', - ]); - expect(projectTomlData['dependency-groups']).toStrictEqual({}); + expect(projectTomlData.project.dependencies).toStrictEqual([ + 'django==5.1.4', + ]); + expect(projectTomlData['dependency-groups']).toStrictEqual({}); - expect(output.success).toBe(true); - }); + expect(output.success).toBe(true); + }); - it('should build python project with local and dev dependencies', async () => { - vol.fromJSON({ - 'apps/app/app1/index.py': 'print("Hello from app")', + it('should build python project with local and dev dependencies', async () => { + vol.fromJSON({ + 'apps/app/app1/index.py': 'print("Hello from app")', - 'apps/app/pyproject.toml': dedent` + 'apps/app/pyproject.toml': dedent` [project] name = "app1" version = "0.1.0" @@ -3390,8 +3391,8 @@ describe('Build Executor', () => { dep1 = { workspace = true } `, - 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', - 'libs/dep1/pyproject.toml': dedent` + 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', + 'libs/dep1/pyproject.toml': dedent` [project] name = "dep1" version = "0.1.0" @@ -3402,124 +3403,124 @@ describe('Build Executor', () => { [tool.hatch.build.targets.wheel] packages = ["dep1"] `, - }); + }); - vi.mocked(spawn.sync) - .mockReturnValueOnce({ - status: 0, - output: [''], - pid: 0, - signal: null, - stderr: null, - stdout: Buffer.from(dedent` - -e ./libs/dep1 - django==5.1.4 - ruff>=0.8.2 - `), - }) - .mockImplementationOnce((_, args, opts) => { - spawnBuildMockImpl(opts); - return { + vi.mocked(spawn.sync) + .mockReturnValueOnce({ 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: true, - lockedVersions: true, - bundleLocalDependencies: true, - }; + stdout: Buffer.from(dedent` + -e ./libs/dep1 + django==5.1.4 + ruff>=0.8.2 + `), + }) + .mockImplementationOnce((_, args, opts) => { + spawnBuildMockImpl(opts); + 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: true, + lockedVersions: true, + bundleLocalDependencies: true, + }; - const output = await executor(options, { - cwd: '', - root: '.', - isVerbose: false, - projectName: 'app', - projectsConfigurations: { - version: 2, - projects: { - app: { - root: 'apps/app', - targets: {}, - }, - dep1: { - root: 'libs/dep1', - targets: {}, + const output = await executor(options, { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + dep1: { + root: 'libs/dep1', + targets: {}, + }, }, }, - }, - nxJsonConfiguration: {}, - projectGraph: { - dependencies: {}, - nodes: {}, - }, - }); + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }); - expect(checkPrerequisites).toHaveBeenCalled(); - console.log('buildPath', buildPath); - expect(existsSync(buildPath)).toBeTruthy(); - expect(existsSync(`${buildPath}/app1`)).toBeTruthy(); - expect(existsSync(`${buildPath}/dep1`)).toBeTruthy(); - expect(existsSync(`${buildPath}/dist/app.fake`)).toBeTruthy(); - expect(spawn.sync).toHaveBeenCalledTimes(2); - expect(spawn.sync).toHaveBeenNthCalledWith( - 1, - 'uv', - [ - 'export', - '--format', - 'requirements-txt', - '--no-hashes', - '--no-header', - '--frozen', - '--no-emit-project', - '--all-extras', - '--project', - 'apps/app', - ], - { - cwd: '.', - shell: true, - stdio: 'pipe', - }, - ); - expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['build'], { - cwd: buildPath, - shell: false, - stdio: 'inherit', - }); + expect(checkPrerequisites).toHaveBeenCalled(); + console.log('buildPath', buildPath); + expect(existsSync(buildPath)).toBeTruthy(); + expect(existsSync(`${buildPath}/app1`)).toBeTruthy(); + expect(existsSync(`${buildPath}/dep1`)).toBeTruthy(); + expect(existsSync(`${buildPath}/dist/app.fake`)).toBeTruthy(); + expect(spawn.sync).toHaveBeenCalledTimes(2); + expect(spawn.sync).toHaveBeenNthCalledWith( + 1, + 'uv', + [ + 'export', + '--format', + 'requirements-txt', + '--no-hashes', + '--no-header', + '--frozen', + '--no-emit-project', + '--all-extras', + '--project', + 'apps/app', + ], + { + cwd: '.', + shell: true, + stdio: 'pipe', + }, + ); + expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['build'], { + cwd: buildPath, + shell: false, + stdio: 'inherit', + }); - const projectTomlData = getPyprojectData( - `${buildPath}/pyproject.toml`, - ); + const projectTomlData = getPyprojectData( + `${buildPath}/pyproject.toml`, + ); - expect( - projectTomlData.tool.hatch.build.targets.wheel.packages, - ).toStrictEqual(['app1', 'dep1']); + expect( + projectTomlData.tool.hatch.build.targets.wheel.packages, + ).toStrictEqual(['app1', 'dep1']); - expect(projectTomlData.project.dependencies).toStrictEqual([ - 'django==5.1.4', - 'ruff>=0.8.2', - ]); - expect(projectTomlData['dependency-groups']).toStrictEqual({}); + expect(projectTomlData.project.dependencies).toStrictEqual([ + 'django==5.1.4', + 'ruff>=0.8.2', + ]); + expect(projectTomlData['dependency-groups']).toStrictEqual({}); - expect(output.success).toBe(true); - }); + expect(output.success).toBe(true); + }); - it('should build python project with local dependencies and delete the build folder', async () => { - vol.fromJSON({ - 'apps/app/app1/index.py': 'print("Hello from app")', + it('should build python project with local dependencies and delete the build folder', async () => { + vol.fromJSON({ + 'apps/app/app1/index.py': 'print("Hello from app")', - 'apps/app/pyproject.toml': dedent` + 'apps/app/pyproject.toml': dedent` [project] name = "app1" version = "0.1.0" @@ -3542,8 +3543,8 @@ describe('Build Executor', () => { dep1 = { workspace = true } `, - 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', - 'libs/dep1/pyproject.toml': dedent` + 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', + 'libs/dep1/pyproject.toml': dedent` [project] name = "dep1" version = "0.1.0" @@ -3554,107 +3555,266 @@ describe('Build Executor', () => { [tool.hatch.build.targets.wheel] packages = ["dep1"] `, - }); + }); - vi.mocked(spawn.sync) - .mockReturnValueOnce({ - status: 0, - output: [''], - pid: 0, - signal: null, - stderr: null, - stdout: Buffer.from(dedent` + vi.mocked(spawn.sync) + .mockReturnValueOnce({ + status: 0, + output: [''], + pid: 0, + signal: null, + stderr: null, + stdout: Buffer.from(dedent` -e ./libs/dep1 django==5.1.4 `), - }) - .mockImplementationOnce((_, args, opts) => { - spawnBuildMockImpl(opts); - return { + }) + .mockImplementationOnce((_, args, opts) => { + spawnBuildMockImpl(opts); + 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: false, + devDependencies: false, + lockedVersions: true, + bundleLocalDependencies: true, + }; + + const output = await executor(options, { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + dep1: { + root: 'libs/dep1', + targets: {}, + }, + }, + }, + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }); + + expect(checkPrerequisites).toHaveBeenCalled(); + expect(existsSync(buildPath)).not.toBeTruthy(); + expect(spawn.sync).toHaveBeenCalledTimes(2); + expect(spawn.sync).toHaveBeenNthCalledWith( + 1, + 'uv', + [ + 'export', + '--format', + 'requirements-txt', + '--no-hashes', + '--no-header', + '--frozen', + '--no-emit-project', + '--all-extras', + '--project', + 'apps/app', + '--no-dev', + ], + { + cwd: '.', + shell: true, + stdio: 'pipe', + }, + ); + expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['build'], { + cwd: buildPath, + shell: false, + stdio: 'inherit', + }); + + expect(output.success).toBe(true); + }); + }); + + describe('project', () => { + beforeEach(() => { + vol.rmSync('uv.lock'); + }); + + it('should build python project with local dependencies', async () => { + vol.fromJSON({ + 'apps/app/app1/index.py': 'print("Hello from app")', + + 'apps/app/pyproject.toml': dedent` + [project] + name = "app1" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "django>=5.1.4", + "dep1", + ] + + [tool.hatch.build.targets.wheel] + packages = ["app1"] + + [dependency-groups] + dev = [ + "ruff>=0.8.2", + ] + + [tool.uv.sources] + dep1 = { path = "../../libs/dep1" } + `, + + 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', + 'libs/dep1/pyproject.toml': dedent` + [project] + name = "dep1" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + + [tool.hatch.build.targets.wheel] + packages = ["dep1"] + `, + }); + + vi.mocked(spawn.sync) + .mockReturnValueOnce({ 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: false, - devDependencies: false, - lockedVersions: true, - bundleLocalDependencies: true, - }; + stdout: Buffer.from(dedent` + ../../libs/dep1 + django==5.1.4 + `), + }) + .mockImplementationOnce((_, args, opts) => { + spawnBuildMockImpl(opts); + 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', - projectsConfigurations: { - version: 2, - projects: { - app: { - root: 'apps/app', - targets: {}, - }, - dep1: { - root: 'libs/dep1', - targets: {}, + const output = await executor(options, { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + dep1: { + root: 'libs/dep1', + targets: {}, + }, }, }, - }, - nxJsonConfiguration: {}, - projectGraph: { - dependencies: {}, - nodes: {}, - }, - }); + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(existsSync(buildPath)).not.toBeTruthy(); - expect(spawn.sync).toHaveBeenCalledTimes(2); - expect(spawn.sync).toHaveBeenNthCalledWith( - 1, - 'uv', - [ - 'export', - '--format', - 'requirements-txt', - '--no-hashes', - '--no-header', - '--frozen', - '--no-emit-project', - '--all-extras', - '--project', - 'apps/app', - '--no-dev', - ], - { - cwd: '.', - shell: true, - stdio: 'pipe', - }, - ); - expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['build'], { - cwd: buildPath, - shell: false, - stdio: 'inherit', - }); + expect(checkPrerequisites).toHaveBeenCalled(); + console.log('buildPath', buildPath); + expect(existsSync(buildPath)).toBeTruthy(); + expect(existsSync(`${buildPath}/app1`)).toBeTruthy(); + expect(existsSync(`${buildPath}/dep1`)).toBeTruthy(); + expect(existsSync(`${buildPath}/dist/app.fake`)).toBeTruthy(); + expect(spawn.sync).toHaveBeenCalledTimes(2); + expect(spawn.sync).toHaveBeenNthCalledWith( + 1, + 'uv', + [ + 'export', + '--format', + 'requirements-txt', + '--no-hashes', + '--no-header', + '--frozen', + '--no-emit-project', + '--all-extras', + '--project', + 'apps/app', + '--no-dev', + ], + { + cwd: '.', + shell: true, + stdio: 'pipe', + }, + ); + expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['build'], { + cwd: buildPath, + shell: false, + stdio: 'inherit', + }); - expect(output.success).toBe(true); + const projectTomlData = getPyprojectData( + `${buildPath}/pyproject.toml`, + ); + + expect( + projectTomlData.tool.hatch.build.targets.wheel.packages, + ).toStrictEqual(['app1', 'dep1']); + + expect(projectTomlData.project.dependencies).toStrictEqual([ + 'django==5.1.4', + ]); + expect(projectTomlData['dependency-groups']).toStrictEqual({}); + + expect(output.success).toBe(true); + }); }); }); describe('project resolver', () => { - it('should build the project without locked versions and without bundle the local dependencies', async () => { - vol.fromJSON({ - 'apps/app/app/index.py': 'print("Hello from app")', - 'apps/app/pyproject.toml': dedent` + describe('workspaces', () => { + it('should build the project without locked versions and without bundle the local dependencies', async () => { + vol.fromJSON({ + 'apps/app/app/index.py': 'print("Hello from app")', + 'apps/app/pyproject.toml': dedent` [project] name = "app" version = "0.1.0" @@ -3677,8 +3837,8 @@ describe('Build Executor', () => { dep1 = { workspace = true } `, - 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', - 'libs/dep1/pyproject.toml': dedent` + 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', + 'libs/dep1/pyproject.toml': dedent` [project] name = "dep1" version = "1.0.0" @@ -3689,7 +3849,7 @@ describe('Build Executor', () => { ] `, - 'uv.lock': dedent` + 'uv.lock': dedent` version = 1 requires-python = ">=3.12" @@ -3730,86 +3890,86 @@ describe('Build Executor', () => { { name = "numpy", specifier = ">=1.21.0" }, ] `, - }); + }); - vi.mocked(spawn.sync).mockImplementation((_, args, opts) => { - spawnBuildMockImpl(opts); - return { - status: 0, - output: [''], - pid: 0, - signal: null, - stderr: null, - stdout: null, - }; - }); + vi.mocked(spawn.sync).mockImplementation((_, args, opts) => { + spawnBuildMockImpl(opts); + 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: false, - bundleLocalDependencies: false, - }; + const options: BuildExecutorSchema = { + ignorePaths: ['.venv', '.tox', 'tests/'], + silent: false, + outputPath: 'dist/apps/app', + keepBuildFolder: true, + devDependencies: false, + lockedVersions: false, + bundleLocalDependencies: false, + }; - const output = await executor(options, { - cwd: '', - root: '.', - isVerbose: false, - projectName: 'app', - projectsConfigurations: { - version: 2, - projects: { - app: { - root: 'apps/app', - targets: {}, - }, - dep1: { - root: 'libs/dep1', + const output = await executor(options, { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + dep1: { + root: 'libs/dep1', + }, }, }, - }, - nxJsonConfiguration: {}, - projectGraph: { - dependencies: {}, - nodes: {}, - }, - }); + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(output.success).toBe(true); - expect(existsSync(buildPath)).toBeTruthy(); - expect(existsSync(`${buildPath}/app`)).toBeTruthy(); - expect(existsSync(`${buildPath}/dep1`)).not.toBeTruthy(); - expect(existsSync(`${buildPath}/dist/app.fake`)).toBeTruthy(); - expect(spawn.sync).toHaveBeenCalledWith('uv', ['build'], { - cwd: buildPath, - shell: false, - stdio: 'inherit', - }); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(output.success).toBe(true); + expect(existsSync(buildPath)).toBeTruthy(); + expect(existsSync(`${buildPath}/app`)).toBeTruthy(); + expect(existsSync(`${buildPath}/dep1`)).not.toBeTruthy(); + expect(existsSync(`${buildPath}/dist/app.fake`)).toBeTruthy(); + expect(spawn.sync).toHaveBeenCalledWith('uv', ['build'], { + cwd: buildPath, + shell: false, + stdio: 'inherit', + }); - const projectTomlData = getPyprojectData( - `${buildPath}/pyproject.toml`, - ); + const projectTomlData = getPyprojectData( + `${buildPath}/pyproject.toml`, + ); - expect( - projectTomlData.tool.hatch.build.targets.wheel.packages, - ).toStrictEqual(['app']); + expect( + projectTomlData.tool.hatch.build.targets.wheel.packages, + ).toStrictEqual(['app']); - expect(projectTomlData.project.dependencies).toStrictEqual([ - 'django>=5.1.4', - 'dep1==1.0.0', - ]); - expect(projectTomlData['dependency-groups']).toStrictEqual({}); - expect(projectTomlData.tool.uv.sources).toStrictEqual({}); - }); + expect(projectTomlData.project.dependencies).toStrictEqual([ + 'django>=5.1.4', + 'dep1==1.0.0', + ]); + expect(projectTomlData['dependency-groups']).toStrictEqual({}); + expect(projectTomlData.tool.uv.sources).toStrictEqual({}); + }); - it('should build the project without locked versions and bundle the local dependencies', async () => { - vol.fromJSON({ - 'apps/app/app/index.py': 'print("Hello from app")', - 'apps/app/pyproject.toml': dedent` + it('should build the project without locked versions and bundle the local dependencies', async () => { + vol.fromJSON({ + 'apps/app/app/index.py': 'print("Hello from app")', + 'apps/app/pyproject.toml': dedent` [project] name = "app" version = "0.1.0" @@ -3832,8 +3992,8 @@ describe('Build Executor', () => { dep1 = { workspace = true } `, - 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', - 'libs/dep1/pyproject.toml': dedent` + 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', + 'libs/dep1/pyproject.toml': dedent` [project] name = "dep1" version = "1.0.0" @@ -3847,7 +4007,7 @@ describe('Build Executor', () => { packages = ["dep1"] `, - 'uv.lock': dedent` + 'uv.lock': dedent` version = 1 requires-python = ">=3.12" @@ -3888,93 +4048,93 @@ describe('Build Executor', () => { { name = "numpy", specifier = ">=1.21.0" }, ] `, - }); + }); - vi.mocked(spawn.sync).mockImplementation((_, args, opts) => { - spawnBuildMockImpl(opts); - return { - status: 0, - output: [''], - pid: 0, - signal: null, - stderr: null, - stdout: null, - }; - }); + vi.mocked(spawn.sync).mockImplementation((_, args, opts) => { + spawnBuildMockImpl(opts); + 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: false, - bundleLocalDependencies: false, - }; + const options: BuildExecutorSchema = { + ignorePaths: ['.venv', '.tox', 'tests/'], + silent: false, + outputPath: 'dist/apps/app', + keepBuildFolder: true, + devDependencies: false, + lockedVersions: false, + bundleLocalDependencies: false, + }; - const output = await executor(options, { - cwd: '', - root: '.', - isVerbose: false, - projectName: 'app', - projectsConfigurations: { - version: 2, - projects: { - app: { - root: 'apps/app', - targets: {}, - }, - dep1: { - root: 'libs/dep1', - targets: { - build: { - options: { - publish: false, + const output = await executor(options, { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + dep1: { + root: 'libs/dep1', + targets: { + build: { + options: { + publish: false, + }, }, }, }, }, }, - }, - nxJsonConfiguration: {}, - projectGraph: { - dependencies: {}, - nodes: {}, - }, - }); + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(output.success).toBe(true); - 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('uv', ['build'], { - cwd: buildPath, - shell: false, - stdio: 'inherit', - }); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(output.success).toBe(true); + 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('uv', ['build'], { + cwd: buildPath, + shell: false, + stdio: 'inherit', + }); - const projectTomlData = getPyprojectData( - `${buildPath}/pyproject.toml`, - ); + const projectTomlData = getPyprojectData( + `${buildPath}/pyproject.toml`, + ); - expect( - projectTomlData.tool.hatch.build.targets.wheel.packages, - ).toStrictEqual(['app', 'dep1']); + expect( + projectTomlData.tool.hatch.build.targets.wheel.packages, + ).toStrictEqual(['app', 'dep1']); - expect(projectTomlData.project.dependencies).toStrictEqual([ - 'django>=5.1.4', - 'numpy>=1.21.0', - ]); - expect(projectTomlData['dependency-groups']).toStrictEqual({}); - expect(projectTomlData.tool.uv.sources).toStrictEqual({}); - }); + expect(projectTomlData.project.dependencies).toStrictEqual([ + 'django>=5.1.4', + 'numpy>=1.21.0', + ]); + expect(projectTomlData['dependency-groups']).toStrictEqual({}); + expect(projectTomlData.tool.uv.sources).toStrictEqual({}); + }); - it('should build the project without locked versions and bundle only local dependency and not the second level', async () => { - vol.fromJSON({ - 'apps/app/app/index.py': 'print("Hello from app")', - 'apps/app/pyproject.toml': dedent` + it('should build the project without locked versions and bundle only local dependency and not the second level', async () => { + vol.fromJSON({ + 'apps/app/app/index.py': 'print("Hello from app")', + 'apps/app/pyproject.toml': dedent` [project] name = "app" version = "0.1.0" @@ -3997,8 +4157,8 @@ describe('Build Executor', () => { dep1 = { workspace = true } `, - 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', - 'libs/dep1/pyproject.toml': dedent` + 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', + 'libs/dep1/pyproject.toml': dedent` [project] name = "dep1" version = "1.0.0" @@ -4016,8 +4176,8 @@ describe('Build Executor', () => { dep2 = { workspace = true } `, - 'libs/dep2/dep2/index.py': 'print("Hello from dep2")', - 'libs/dep2/pyproject.toml': dedent` + 'libs/dep2/dep2/index.py': 'print("Hello from dep2")', + 'libs/dep2/pyproject.toml': dedent` [project] name = "dep2" version = "1.0.0" @@ -4031,7 +4191,7 @@ describe('Build Executor', () => { packages = ["dep2"] `, - 'uv.lock': dedent` + 'uv.lock': dedent` version = 1 requires-python = ">=3.12" @@ -4087,116 +4247,116 @@ describe('Build Executor', () => { { name = "requests", specifier = ">=2.32.3" }, ] `, - }); + }); - vi.mocked(spawn.sync).mockImplementation((_, args, opts) => { - spawnBuildMockImpl(opts); - return { - status: 0, - output: [''], - pid: 0, - signal: null, - stderr: null, - stdout: null, - }; - }); + vi.mocked(spawn.sync).mockImplementation((_, args, opts) => { + spawnBuildMockImpl(opts); + 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: false, - bundleLocalDependencies: false, - }; + const options: BuildExecutorSchema = { + ignorePaths: ['.venv', '.tox', 'tests/'], + silent: false, + outputPath: 'dist/apps/app', + keepBuildFolder: true, + devDependencies: false, + lockedVersions: false, + bundleLocalDependencies: false, + }; - const output = await executor(options, { - cwd: '', - root: '.', - isVerbose: false, - projectName: 'app', - projectsConfigurations: { - version: 2, - projects: { - app: { - root: 'apps/app', - targets: {}, - }, - dep1: { - root: 'libs/dep1', - targets: { - build: { - options: { - publish: false, + const output = await executor(options, { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + dep1: { + root: 'libs/dep1', + targets: { + build: { + options: { + publish: false, + }, }, }, }, - }, - dep2: { - root: 'libs/dep2', - targets: { - build: { - options: { - publish: true, - customSourceName: 'foo', - customSourceUrl: 'http://example.com/bar', + dep2: { + root: 'libs/dep2', + targets: { + build: { + options: { + publish: true, + customSourceName: 'foo', + customSourceUrl: 'http://example.com/bar', + }, }, }, }, }, }, - }, - nxJsonConfiguration: {}, - projectGraph: { - dependencies: {}, - nodes: {}, - }, - }); + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(output.success).toBe(true); - 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('uv', ['build'], { - cwd: buildPath, - shell: false, - stdio: 'inherit', - }); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(output.success).toBe(true); + 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('uv', ['build'], { + cwd: buildPath, + shell: false, + stdio: 'inherit', + }); - const projectTomlData = getPyprojectData( - `${buildPath}/pyproject.toml`, - ); + const projectTomlData = getPyprojectData( + `${buildPath}/pyproject.toml`, + ); - expect( - projectTomlData.tool.hatch.build.targets.wheel.packages, - ).toStrictEqual(['app', 'dep1']); + expect( + projectTomlData.tool.hatch.build.targets.wheel.packages, + ).toStrictEqual(['app', 'dep1']); - expect(projectTomlData.project.dependencies).toStrictEqual([ - 'django>=5.1.4', - 'numpy>=1.21.0', - 'dep2==1.0.0', - ]); + expect(projectTomlData.project.dependencies).toStrictEqual([ + 'django>=5.1.4', + 'numpy>=1.21.0', + 'dep2==1.0.0', + ]); - expect(projectTomlData['dependency-groups']).toStrictEqual({}); - expect(projectTomlData.tool.uv.sources).toStrictEqual({ - dep2: { index: 'foo' }, - }); + expect(projectTomlData['dependency-groups']).toStrictEqual({}); + expect(projectTomlData.tool.uv.sources).toStrictEqual({ + dep2: { index: 'foo' }, + }); - expect(projectTomlData.tool.uv.index).toStrictEqual([ - { - name: 'foo', - url: 'http://example.com/bar', - }, - ]); - }); + expect(projectTomlData.tool.uv.index).toStrictEqual([ + { + name: 'foo', + url: 'http://example.com/bar', + }, + ]); + }); - it('should build the project without locked versions and handle duplicate sources', async () => { - vol.fromJSON({ - 'apps/app/app/index.py': 'print("Hello from app")', - 'apps/app/pyproject.toml': dedent` + it('should build the project without locked versions and handle duplicate sources', async () => { + vol.fromJSON({ + 'apps/app/app/index.py': 'print("Hello from app")', + 'apps/app/pyproject.toml': dedent` [project] name = "app" version = "0.1.0" @@ -4227,8 +4387,8 @@ describe('Build Executor', () => { dep5 = { workspace = true } `, - 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', - 'libs/dep1/pyproject.toml': dedent` + 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', + 'libs/dep1/pyproject.toml': dedent` [project] name = "dep1" version = "1.0.0" @@ -4246,8 +4406,8 @@ describe('Build Executor', () => { dep2 = { workspace = true } `, - 'libs/dep2/dep2/index.py': 'print("Hello from dep2")', - 'libs/dep2/pyproject.toml': dedent` + 'libs/dep2/dep2/index.py': 'print("Hello from dep2")', + 'libs/dep2/pyproject.toml': dedent` [project] name = "dep2" version = "1.0.0" @@ -4260,8 +4420,8 @@ describe('Build Executor', () => { [tool.hatch.build.targets.wheel] packages = ["dep2"] `, - 'libs/dep3/dep3/index.py': 'print("Hello from dep3")', - 'libs/dep3/pyproject.toml': dedent` + 'libs/dep3/dep3/index.py': 'print("Hello from dep3")', + 'libs/dep3/pyproject.toml': dedent` [project] name = "dep3" version = "1.0.0" @@ -4274,8 +4434,8 @@ describe('Build Executor', () => { [tool.hatch.build.targets.wheel] packages = ["dep3"] `, - 'libs/dep4/dep4/index.py': 'print("Hello from dep4")', - 'libs/dep4/pyproject.toml': dedent` + 'libs/dep4/dep4/index.py': 'print("Hello from dep4")', + 'libs/dep4/pyproject.toml': dedent` [project] name = "dep4" version = "1.0.0" @@ -4288,8 +4448,8 @@ describe('Build Executor', () => { [tool.hatch.build.targets.wheel] packages = ["dep4"] `, - 'libs/dep5/dep5/index.py': 'print("Hello from dep5")', - 'libs/dep5/pyproject.toml': dedent` + 'libs/dep5/dep5/index.py': 'print("Hello from dep5")', + 'libs/dep5/pyproject.toml': dedent` [project] name = "dep5" version = "1.0.0" @@ -4303,7 +4463,7 @@ describe('Build Executor', () => { packages = ["dep5"] `, - 'uv.lock': dedent` + 'uv.lock': dedent` version = 1 requires-python = ">=3.12" @@ -4406,170 +4566,854 @@ describe('Build Executor', () => { { name = "requests", specifier = ">=2.32.3" }, ] `, - }); + }); - vi.mocked(spawn.sync).mockImplementation((_, args, opts) => { - spawnBuildMockImpl(opts); - return { - status: 0, - output: [''], - pid: 0, - signal: null, - stderr: null, - stdout: null, + vi.mocked(spawn.sync).mockImplementation((_, args, opts) => { + spawnBuildMockImpl(opts); + 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: false, + bundleLocalDependencies: false, }; + + const output = await executor(options, { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + dep1: { + root: 'libs/dep1', + targets: { + build: { + options: { + publish: true, + customSourceName: 'foo', + customSourceUrl: 'http://example.com/foo', + }, + }, + }, + }, + dep2: { + root: 'libs/dep2', + targets: { + build: { + options: { + publish: true, + customSourceName: 'foo', + customSourceUrl: 'http://example.com/bar', + }, + }, + }, + }, + dep3: { + root: 'libs/dep3', + targets: { + build: { + options: { + publish: true, + customSourceName: 'foo', + customSourceUrl: 'http://example.com/bar', + }, + }, + }, + }, + dep4: { + root: 'libs/dep4', + targets: { + build: { + options: { + publish: true, + customSourceName: 'another', + customSourceUrl: 'http://example.com/another', + }, + }, + }, + }, + dep5: { + root: 'libs/dep5', + targets: { + build: { + options: { + publish: true, + customSourceName: 'another', + customSourceUrl: 'http://example.com/another', + }, + }, + }, + }, + }, + }, + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }); + + expect(checkPrerequisites).toHaveBeenCalled(); + expect(output.success).toBe(true); + expect(existsSync(buildPath)).toBeTruthy(); + expect(existsSync(`${buildPath}/app`)).toBeTruthy(); + expect(existsSync(`${buildPath}/dist/app.fake`)).toBeTruthy(); + expect(spawn.sync).toHaveBeenCalledWith('uv', ['build'], { + cwd: buildPath, + shell: false, + stdio: 'inherit', + }); + + const projectTomlData = getPyprojectData( + `${buildPath}/pyproject.toml`, + ); + + expect(projectTomlData.tool.uv.index).toStrictEqual([ + { + name: 'foo', + url: 'http://example.com/foo', + }, + { + name: 'foo-198fb9d8236b3d9116a180365e447b05', + url: 'http://example.com/bar', + }, + { + name: 'another', + url: 'http://example.com/another', + }, + ]); + + expect( + projectTomlData.tool.hatch.build.targets.wheel.packages, + ).toStrictEqual(['app']); + + expect(projectTomlData.project.dependencies).toStrictEqual([ + 'django>=5.1.4', + 'dep1==1.0.0', + 'dep2==1.0.0', + 'dep3==1.0.0', + 'dep4==1.0.0', + 'dep5==1.0.0', + ]); + expect(projectTomlData['dependency-groups']).toStrictEqual({}); + expect(projectTomlData.tool.uv.sources).toStrictEqual({ + dep1: { + index: 'foo', + }, + dep2: { + index: 'foo-198fb9d8236b3d9116a180365e447b05', + }, + dep3: { + index: 'foo-198fb9d8236b3d9116a180365e447b05', + }, + dep4: { + index: 'another', + }, + dep5: { + index: 'another', + }, + }); }); + }); - const options: BuildExecutorSchema = { - ignorePaths: ['.venv', '.tox', 'tests/'], - silent: false, - outputPath: 'dist/apps/app', - keepBuildFolder: true, - devDependencies: false, - lockedVersions: false, - bundleLocalDependencies: false, - }; + describe('project', () => { + beforeEach(() => { + vol.rmSync('uv.lock'); + }); - const output = await executor(options, { - cwd: '', - root: '.', - isVerbose: false, - projectName: 'app', - projectsConfigurations: { - version: 2, - projects: { - app: { - root: 'apps/app', - targets: {}, + it('should build the project without locked versions and without bundle the local dependencies', async () => { + vol.fromJSON({ + 'apps/app/app/index.py': 'print("Hello from app")', + 'apps/app/pyproject.toml': dedent` + [project] + name = "app" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "django>=5.1.4", + "dep1", + ] + + [tool.hatch.build.targets.wheel] + packages = ["app"] + + [dependency-groups] + dev = [ + "ruff>=0.8.2", + ] + + [tool.uv.sources] + dep1 = { path = "../../libs/dep1" } + `, + + 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', + 'libs/dep1/pyproject.toml': dedent` + [project] + name = "dep1" + version = "1.0.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "numpy>=1.21.0" + ] + `, + }); + + vi.mocked(spawn.sync).mockImplementation((_, args, opts) => { + spawnBuildMockImpl(opts); + 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: false, + bundleLocalDependencies: false, + }; + + const output = await executor(options, { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + dep1: { + root: 'libs/dep1', + }, }, - dep1: { - root: 'libs/dep1', - targets: { - build: { - options: { - publish: true, - customSourceName: 'foo', - customSourceUrl: 'http://example.com/foo', + }, + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }); + + expect(checkPrerequisites).toHaveBeenCalled(); + expect(output.success).toBe(true); + expect(existsSync(buildPath)).toBeTruthy(); + expect(existsSync(`${buildPath}/app`)).toBeTruthy(); + expect(existsSync(`${buildPath}/dep1`)).not.toBeTruthy(); + expect(existsSync(`${buildPath}/dist/app.fake`)).toBeTruthy(); + expect(spawn.sync).toHaveBeenCalledWith('uv', ['build'], { + cwd: buildPath, + shell: false, + stdio: 'inherit', + }); + + const projectTomlData = getPyprojectData( + `${buildPath}/pyproject.toml`, + ); + + expect( + projectTomlData.tool.hatch.build.targets.wheel.packages, + ).toStrictEqual(['app']); + + expect(projectTomlData.project.dependencies).toStrictEqual([ + 'django>=5.1.4', + 'dep1==1.0.0', + ]); + expect(projectTomlData['dependency-groups']).toStrictEqual({}); + expect(projectTomlData.tool.uv.sources).toStrictEqual({}); + }); + + it('should build the project without locked versions and bundle the local dependencies', async () => { + vol.fromJSON({ + 'apps/app/app/index.py': 'print("Hello from app")', + 'apps/app/pyproject.toml': dedent` + [project] + name = "app" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "django>=5.1.4", + "dep1", + ] + + [tool.hatch.build.targets.wheel] + packages = ["app"] + + [dependency-groups] + dev = [ + "ruff>=0.8.2", + ] + + [tool.uv.sources] + dep1 = { path = "../../libs/dep1" } + `, + + 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', + 'libs/dep1/pyproject.toml': dedent` + [project] + name = "dep1" + version = "1.0.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "numpy>=1.21.0" + ] + + [tool.hatch.build.targets.wheel] + packages = ["dep1"] + `, + }); + + vi.mocked(spawn.sync).mockImplementation((_, args, opts) => { + spawnBuildMockImpl(opts); + 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: false, + bundleLocalDependencies: false, + }; + + const output = await executor(options, { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + dep1: { + root: 'libs/dep1', + targets: { + build: { + options: { + publish: false, + }, }, }, }, }, - dep2: { - root: 'libs/dep2', - targets: { - build: { - options: { - publish: true, - customSourceName: 'foo', - customSourceUrl: 'http://example.com/bar', + }, + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }); + + expect(checkPrerequisites).toHaveBeenCalled(); + expect(output.success).toBe(true); + 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('uv', ['build'], { + cwd: buildPath, + shell: false, + stdio: 'inherit', + }); + + const projectTomlData = getPyprojectData( + `${buildPath}/pyproject.toml`, + ); + + expect( + projectTomlData.tool.hatch.build.targets.wheel.packages, + ).toStrictEqual(['app', 'dep1']); + + expect(projectTomlData.project.dependencies).toStrictEqual([ + 'django>=5.1.4', + 'numpy>=1.21.0', + ]); + expect(projectTomlData['dependency-groups']).toStrictEqual({}); + expect(projectTomlData.tool.uv.sources).toStrictEqual({}); + }); + + it('should build the project without locked versions and bundle only local dependency and not the second level', async () => { + vol.fromJSON({ + 'apps/app/app/index.py': 'print("Hello from app")', + 'apps/app/pyproject.toml': dedent` + [project] + name = "app" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "django>=5.1.4", + "dep1", + ] + + [tool.hatch.build.targets.wheel] + packages = ["app"] + + [dependency-groups] + dev = [ + "ruff>=0.8.2", + ] + + [tool.uv.sources] + dep1 = { path = "../../libs/dep1" } + `, + + 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', + 'libs/dep1/pyproject.toml': dedent` + [project] + name = "dep1" + version = "1.0.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "numpy>=1.21.0", + "dep2" + ] + + [tool.hatch.build.targets.wheel] + packages = ["dep1"] + + [tool.uv.sources] + dep2 = { path = "../dep2" } + `, + + 'libs/dep2/dep2/index.py': 'print("Hello from dep2")', + 'libs/dep2/pyproject.toml': dedent` + [project] + name = "dep2" + version = "1.0.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "requests>=2.32.3" + ] + + [tool.hatch.build.targets.wheel] + packages = ["dep2"] + `, + }); + + vi.mocked(spawn.sync).mockImplementation((_, args, opts) => { + spawnBuildMockImpl(opts); + 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: false, + bundleLocalDependencies: false, + }; + + const output = await executor(options, { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + dep1: { + root: 'libs/dep1', + targets: { + build: { + options: { + publish: false, + }, }, }, }, - }, - dep3: { - root: 'libs/dep3', - targets: { - build: { - options: { - publish: true, - customSourceName: 'foo', - customSourceUrl: 'http://example.com/bar', + dep2: { + root: 'libs/dep2', + targets: { + build: { + options: { + publish: true, + customSourceName: 'foo', + customSourceUrl: 'http://example.com/bar', + }, }, }, }, }, - dep4: { - root: 'libs/dep4', - targets: { - build: { - options: { - publish: true, - customSourceName: 'another', - customSourceUrl: 'http://example.com/another', + }, + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }); + + expect(checkPrerequisites).toHaveBeenCalled(); + expect(output.success).toBe(true); + 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('uv', ['build'], { + cwd: buildPath, + shell: false, + stdio: 'inherit', + }); + + const projectTomlData = getPyprojectData( + `${buildPath}/pyproject.toml`, + ); + + expect( + projectTomlData.tool.hatch.build.targets.wheel.packages, + ).toStrictEqual(['app', 'dep1']); + + expect(projectTomlData.project.dependencies).toStrictEqual([ + 'django>=5.1.4', + 'numpy>=1.21.0', + 'dep2==1.0.0', + ]); + + expect(projectTomlData['dependency-groups']).toStrictEqual({}); + expect(projectTomlData.tool.uv.sources).toStrictEqual({ + dep2: { index: 'foo' }, + }); + + expect(projectTomlData.tool.uv.index).toStrictEqual([ + { + name: 'foo', + url: 'http://example.com/bar', + }, + ]); + }); + + it('should build the project without locked versions and handle duplicate sources', async () => { + vol.fromJSON({ + 'apps/app/app/index.py': 'print("Hello from app")', + 'apps/app/pyproject.toml': dedent` + [project] + name = "app" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "django>=5.1.4", + "dep1", + "dep2", + "dep3", + "dep4", + "dep5", + ] + + [tool.hatch.build.targets.wheel] + packages = ["app"] + + [dependency-groups] + dev = [ + "ruff>=0.8.2", + ] + + [tool.uv.sources] + dep1 = { path = "../../libs/dep1" } + dep2 = { path = "../../libs/dep2" } + dep3 = { path = "../../libs/dep3" } + dep4 = { path = "../../libs/dep4" } + dep5 = { path = "../../libs/dep5" } + `, + + 'libs/dep1/dep1/index.py': 'print("Hello from dep1")', + 'libs/dep1/pyproject.toml': dedent` + [project] + name = "dep1" + version = "1.0.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "numpy>=1.21.0", + "dep2" + ] + + [tool.hatch.build.targets.wheel] + packages = ["dep1"] + + [tool.uv.sources] + dep2 = { path = "../dep2" } + `, + + 'libs/dep2/dep2/index.py': 'print("Hello from dep2")', + 'libs/dep2/pyproject.toml': dedent` + [project] + name = "dep2" + version = "1.0.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "requests>=2.32.3" + ] + + [tool.hatch.build.targets.wheel] + packages = ["dep2"] + `, + 'libs/dep3/dep3/index.py': 'print("Hello from dep3")', + 'libs/dep3/pyproject.toml': dedent` + [project] + name = "dep3" + version = "1.0.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "requests>=2.32.3" + ] + + [tool.hatch.build.targets.wheel] + packages = ["dep3"] + `, + 'libs/dep4/dep4/index.py': 'print("Hello from dep4")', + 'libs/dep4/pyproject.toml': dedent` + [project] + name = "dep4" + version = "1.0.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "requests>=2.32.3" + ] + + [tool.hatch.build.targets.wheel] + packages = ["dep4"] + `, + 'libs/dep5/dep5/index.py': 'print("Hello from dep5")', + 'libs/dep5/pyproject.toml': dedent` + [project] + name = "dep5" + version = "1.0.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "requests>=2.32.3" + ] + + [tool.hatch.build.targets.wheel] + packages = ["dep5"] + `, + }); + + vi.mocked(spawn.sync).mockImplementation((_, args, opts) => { + spawnBuildMockImpl(opts); + 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: false, + bundleLocalDependencies: false, + }; + + const output = await executor(options, { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + dep1: { + root: 'libs/dep1', + targets: { + build: { + options: { + publish: true, + customSourceName: 'foo', + customSourceUrl: 'http://example.com/foo', + }, }, }, }, - }, - dep5: { - root: 'libs/dep5', - targets: { - build: { - options: { - publish: true, - customSourceName: 'another', - customSourceUrl: 'http://example.com/another', + dep2: { + root: 'libs/dep2', + targets: { + build: { + options: { + publish: true, + customSourceName: 'foo', + customSourceUrl: 'http://example.com/bar', + }, + }, + }, + }, + dep3: { + root: 'libs/dep3', + targets: { + build: { + options: { + publish: true, + customSourceName: 'foo', + customSourceUrl: 'http://example.com/bar', + }, + }, + }, + }, + dep4: { + root: 'libs/dep4', + targets: { + build: { + options: { + publish: true, + customSourceName: 'another', + customSourceUrl: 'http://example.com/another', + }, + }, + }, + }, + dep5: { + root: 'libs/dep5', + targets: { + build: { + options: { + publish: true, + customSourceName: 'another', + customSourceUrl: 'http://example.com/another', + }, }, }, }, }, }, - }, - nxJsonConfiguration: {}, - projectGraph: { - dependencies: {}, - nodes: {}, - }, - }); - - expect(checkPrerequisites).toHaveBeenCalled(); - expect(output.success).toBe(true); - expect(existsSync(buildPath)).toBeTruthy(); - expect(existsSync(`${buildPath}/app`)).toBeTruthy(); - expect(existsSync(`${buildPath}/dist/app.fake`)).toBeTruthy(); - expect(spawn.sync).toHaveBeenCalledWith('uv', ['build'], { - cwd: buildPath, - shell: false, - stdio: 'inherit', - }); + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }); - const projectTomlData = getPyprojectData( - `${buildPath}/pyproject.toml`, - ); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(output.success).toBe(true); + expect(existsSync(buildPath)).toBeTruthy(); + expect(existsSync(`${buildPath}/app`)).toBeTruthy(); + expect(existsSync(`${buildPath}/dist/app.fake`)).toBeTruthy(); + expect(spawn.sync).toHaveBeenCalledWith('uv', ['build'], { + cwd: buildPath, + shell: false, + stdio: 'inherit', + }); - expect(projectTomlData.tool.uv.index).toStrictEqual([ - { - name: 'foo', - url: 'http://example.com/foo', - }, - { - name: 'foo-198fb9d8236b3d9116a180365e447b05', - url: 'http://example.com/bar', - }, - { - name: 'another', - url: 'http://example.com/another', - }, - ]); + const projectTomlData = getPyprojectData( + `${buildPath}/pyproject.toml`, + ); - expect( - projectTomlData.tool.hatch.build.targets.wheel.packages, - ).toStrictEqual(['app']); - - expect(projectTomlData.project.dependencies).toStrictEqual([ - 'django>=5.1.4', - 'dep1==1.0.0', - 'dep2==1.0.0', - 'dep3==1.0.0', - 'dep4==1.0.0', - 'dep5==1.0.0', - ]); - expect(projectTomlData['dependency-groups']).toStrictEqual({}); - expect(projectTomlData.tool.uv.sources).toStrictEqual({ - dep1: { - index: 'foo', - }, - dep2: { - index: 'foo-198fb9d8236b3d9116a180365e447b05', - }, - dep3: { - index: 'foo-198fb9d8236b3d9116a180365e447b05', - }, - dep4: { - index: 'another', - }, - dep5: { - index: 'another', - }, + expect(projectTomlData.tool.uv.index).toStrictEqual([ + { + name: 'foo', + url: 'http://example.com/foo', + }, + { + name: 'foo-198fb9d8236b3d9116a180365e447b05', + url: 'http://example.com/bar', + }, + { + name: 'another', + url: 'http://example.com/another', + }, + ]); + + expect( + projectTomlData.tool.hatch.build.targets.wheel.packages, + ).toStrictEqual(['app']); + + expect(projectTomlData.project.dependencies).toStrictEqual([ + 'django>=5.1.4', + 'dep1==1.0.0', + 'dep2==1.0.0', + 'dep3==1.0.0', + 'dep4==1.0.0', + 'dep5==1.0.0', + ]); + expect(projectTomlData['dependency-groups']).toStrictEqual({}); + expect(projectTomlData.tool.uv.sources).toStrictEqual({ + dep1: { + index: 'foo', + }, + dep2: { + index: 'foo-198fb9d8236b3d9116a180365e447b05', + }, + dep3: { + index: 'foo-198fb9d8236b3d9116a180365e447b05', + }, + dep4: { + index: 'another', + }, + dep5: { + index: 'another', + }, + }); }); }); }); diff --git a/packages/nx-python/src/executors/build/executor.ts b/packages/nx-python/src/executors/build/executor.ts index 3b3774c..8fecbc9 100644 --- a/packages/nx-python/src/executors/build/executor.ts +++ b/packages/nx-python/src/executors/build/executor.ts @@ -14,7 +14,12 @@ export default async function executor( const workspaceRoot = context.root; process.chdir(workspaceRoot); try { - const provider = await getProvider(context.root, logger); + const provider = await getProvider( + context.root, + logger, + undefined, + context, + ); const buildFolderPath = await provider.build(options, context); return { diff --git a/packages/nx-python/src/executors/install/executor.spec.ts b/packages/nx-python/src/executors/install/executor.spec.ts index 843680f..76f8ddb 100644 --- a/packages/nx-python/src/executors/install/executor.spec.ts +++ b/packages/nx-python/src/executors/install/executor.spec.ts @@ -8,6 +8,7 @@ import path from 'path'; import spawn from 'cross-spawn'; import { ExecutorContext } from '@nx/devkit'; import { UVProvider } from '../../provider/uv'; +import dedent from 'string-dedent'; describe('Install Executor', () => { const context: ExecutorContext = { @@ -231,158 +232,192 @@ describe('Install Executor', () => { vi.spyOn(process, 'chdir').mockReturnValue(undefined); }); - beforeEach(() => { - vol.fromJSON({ - 'uv.lock': '', + describe('worskpace', () => { + beforeEach(() => { + vol.fromJSON({ + 'uv.lock': '', + }); }); - }); - it('should return success false when the uv is not installed', async () => { - checkPrerequisites.mockRejectedValue(new Error('uv not found')); - - const options = { - silent: false, - debug: false, - verbose: false, - }; - - const context: ExecutorContext = { - cwd: '', - root: '.', - isVerbose: false, - projectName: 'app', - projectsConfigurations: { - version: 2, - projects: { - app: { - root: 'apps/app', - targets: {}, + it('should return success false when the uv is not installed', async () => { + checkPrerequisites.mockRejectedValue(new Error('uv not found')); + + const options = { + silent: false, + debug: false, + verbose: false, + }; + + const context: ExecutorContext = { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, }, }, - }, - nxJsonConfiguration: {}, - projectGraph: { - dependencies: {}, - nodes: {}, - }, - }; + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }; - const output = await executor(options, context); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(spawn.sync).not.toHaveBeenCalled(); - expect(output.success).toBe(false); - }); + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).not.toHaveBeenCalled(); + expect(output.success).toBe(false); + }); - it('should install the dependencies using default values', async () => { - const options = { - silent: false, - debug: false, - verbose: false, - }; + it('should install the dependencies using default values', async () => { + const options = { + silent: false, + debug: false, + verbose: false, + }; - const output = await executor(options, context); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(spawn.sync).toHaveBeenCalledWith('uv', ['sync'], { - stdio: 'inherit', - shell: false, - cwd: '.', + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).toHaveBeenCalledWith('uv', ['sync'], { + stdio: 'inherit', + shell: false, + cwd: '.', + }); + expect(output.success).toBe(true); }); - expect(output.success).toBe(true); - }); - - it('should install the dependencies with args', async () => { - const options = { - silent: false, - debug: false, - verbose: false, - args: '--no-dev', - }; - const output = await executor(options, context); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(spawn.sync).toHaveBeenCalledWith('uv', ['sync', '--no-dev'], { - stdio: 'inherit', - shell: false, - cwd: '.', + it('should install the dependencies with args', async () => { + const options = { + silent: false, + debug: false, + verbose: false, + args: '--no-dev', + }; + + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).toHaveBeenCalledWith('uv', ['sync', '--no-dev'], { + stdio: 'inherit', + shell: false, + cwd: '.', + }); + expect(output.success).toBe(true); }); - expect(output.success).toBe(true); - }); - it('should install the dependencies with verbose flag', async () => { - const options = { - silent: false, - debug: false, - verbose: true, - }; + it('should install the dependencies with verbose flag', async () => { + const options = { + silent: false, + debug: false, + verbose: true, + }; - const output = await executor(options, context); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(spawn.sync).toHaveBeenCalledWith('uv', ['sync', '-v'], { - stdio: 'inherit', - shell: false, - cwd: '.', + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).toHaveBeenCalledWith('uv', ['sync', '-v'], { + stdio: 'inherit', + shell: false, + cwd: '.', + }); + expect(output.success).toBe(true); }); - expect(output.success).toBe(true); - }); - it('should install the dependencies with debug flag', async () => { - const options = { - silent: false, - debug: true, - verbose: false, - }; + it('should install the dependencies with debug flag', async () => { + const options = { + silent: false, + debug: true, + verbose: false, + }; - const output = await executor(options, context); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(spawn.sync).toHaveBeenCalledWith('uv', ['sync', '-vvv'], { - stdio: 'inherit', - shell: false, - cwd: '.', + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).toHaveBeenCalledWith('uv', ['sync', '-vvv'], { + stdio: 'inherit', + shell: false, + cwd: '.', + }); + expect(output.success).toBe(true); }); - expect(output.success).toBe(true); - }); - it('should install the dependencies with custom cache dir', async () => { - const options = { - silent: false, - debug: false, - verbose: false, - cacheDir: 'apps/app/.cache/custom', - }; + it('should install the dependencies with custom cache dir', async () => { + const options = { + silent: false, + debug: false, + verbose: false, + cacheDir: 'apps/app/.cache/custom', + }; + + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).toHaveBeenCalledWith( + 'uv', + ['sync', '--cache-dir', 'apps/app/.cache/custom'], + { + stdio: 'inherit', + cwd: '.', + shell: false, + }, + ); + expect(output.success).toBe(true); + }); - const output = await executor(options, context); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(spawn.sync).toHaveBeenCalledWith( - 'uv', - ['sync', '--cache-dir', 'apps/app/.cache/custom'], - { + it('should not install when the command fail', async () => { + vi.mocked(spawn.sync).mockImplementation(() => { + throw new Error('fake'); + }); + + const options = { + silent: false, + debug: false, + verbose: false, + }; + + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).toHaveBeenCalledWith('uv', ['sync'], { stdio: 'inherit', - cwd: '.', shell: false, - }, - ); - expect(output.success).toBe(true); + cwd: '.', + }); + expect(output.success).toBe(false); + }); }); - it('should not install when the command fail', async () => { - vi.mocked(spawn.sync).mockImplementation(() => { - throw new Error('fake'); + describe('project', () => { + beforeEach(() => { + vol.fromJSON({ + 'apps/app/pyproject.toml': dedent` + [project] + name = "app" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + `, + }); }); - const options = { - silent: false, - debug: false, - verbose: false, - }; + it('should install the dependencies using default values', async () => { + const options = { + silent: false, + debug: false, + verbose: false, + }; - const output = await executor(options, context); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(spawn.sync).toHaveBeenCalledWith('uv', ['sync'], { - stdio: 'inherit', - shell: false, - cwd: '.', + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).toHaveBeenCalledWith('uv', ['sync'], { + stdio: 'inherit', + shell: false, + cwd: 'apps/app', + }); + expect(output.success).toBe(true); }); - expect(output.success).toBe(false); }); }); }); diff --git a/packages/nx-python/src/executors/install/executor.ts b/packages/nx-python/src/executors/install/executor.ts index 90cb84d..79ab251 100644 --- a/packages/nx-python/src/executors/install/executor.ts +++ b/packages/nx-python/src/executors/install/executor.ts @@ -14,7 +14,12 @@ export default async function executor( const workspaceRoot = context.root; process.chdir(workspaceRoot); try { - const provider = await getProvider(workspaceRoot, logger); + const provider = await getProvider( + workspaceRoot, + logger, + undefined, + context, + ); await provider.install(options, context); return { diff --git a/packages/nx-python/src/executors/remove/executor.spec.ts b/packages/nx-python/src/executors/remove/executor.spec.ts index fd1092d..4a7979f 100644 --- a/packages/nx-python/src/executors/remove/executor.spec.ts +++ b/packages/nx-python/src/executors/remove/executor.spec.ts @@ -686,89 +686,232 @@ describe('Delete Executor', () => { vi.spyOn(process, 'chdir').mockReturnValue(undefined); }); - beforeEach(() => { - vol.fromJSON({ - 'uv.lock': '', + describe('workspace', () => { + beforeEach(() => { + vol.fromJSON({ + 'uv.lock': '', + }); }); - }); - it('should return success false when the uv is not installed', async () => { - checkPrerequisites.mockRejectedValue(new Error('uv not found')); + it('should return success false when the uv is not installed', async () => { + checkPrerequisites.mockRejectedValue(new Error('uv not found')); + + const options = { + name: 'shared1', + local: true, + }; + + const context: ExecutorContext = { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + }, + }, + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }; - const options = { - name: 'shared1', - local: true, - }; + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).not.toHaveBeenCalled(); + expect(output.success).toBe(false); + }); - const context: ExecutorContext = { - cwd: '', - root: '.', - isVerbose: false, - projectName: 'app', - projectsConfigurations: { - version: 2, - projects: { - app: { - root: 'apps/app', - targets: {}, + it('should remove external dependency with args', async () => { + const options = { + name: 'click', + local: false, + args: '-vvv', + }; + + const context: ExecutorContext = { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, }, }, - }, - nxJsonConfiguration: {}, - projectGraph: { - dependencies: {}, - nodes: {}, - }, - }; - - const output = await executor(options, context); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(spawn.sync).not.toHaveBeenCalled(); - expect(output.success).toBe(false); + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }; + + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).toHaveBeenCalledTimes(1); + expect(spawn.sync).toHaveBeenNthCalledWith( + 1, + 'uv', + ['remove', 'click', '--project', 'apps/app', '-vvv'], + { + cwd: '.', + shell: false, + stdio: 'inherit', + }, + ); + expect(output.success).toBe(true); + }); }); - it('should remove external dependency with args', async () => { - const options = { - name: 'click', - local: false, - args: '-vvv', - }; - - const context: ExecutorContext = { - cwd: '', - root: '.', - isVerbose: false, - projectName: 'app', - projectsConfigurations: { - version: 2, - projects: { - app: { - root: 'apps/app', - targets: {}, + describe('project', () => { + it('run remove target and should update all the dependency tree', async () => { + vol.fromJSON({ + 'apps/app/pyproject.toml': dedent` + [project] + name = "app" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "lib1", + ] + + [tool.hatch.build.targets.wheel] + packages = ["app"] + + [tool.uv.sources] + lib1 = { path = "../../libs/lib1" } + `, + + 'apps/app1/pyproject.toml': dedent` + [project] + name = "app1" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "lib1", + ] + + [tool.hatch.build.targets.wheel] + packages = ["app1"] + + [tool.uv.sources] + lib1 = { path = "../../libs/lib1" } + `, + + 'libs/lib1/pyproject.toml': dedent` + [project] + name = "lib1" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "shared1", + ] + + [tool.hatch.build.targets.wheel] + packages = ["lib1"] + + [tool.uv.sources] + shared1 = { path = "../shared1" } + `, + + 'libs/shared1/pyproject.toml': dedent` + [project] + name = "shared1" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + + [tool.hatch.build.targets.wheel] + packages = ["shared1"] + `, + }); + + const options = { + name: 'numpy', + local: false, + }; + + const context: ExecutorContext = { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'shared1', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + app1: { + root: 'apps/app1', + targets: {}, + }, + app3: { + root: 'apps/app3', + targets: {}, + }, + lib1: { + root: 'libs/lib1', + targets: {}, + }, + shared1: { + root: 'libs/shared1', + targets: {}, + }, }, }, - }, - nxJsonConfiguration: {}, - projectGraph: { - dependencies: {}, - nodes: {}, - }, - }; - - const output = await executor(options, context); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(spawn.sync).toHaveBeenCalledTimes(1); - expect(spawn.sync).toHaveBeenNthCalledWith( - 1, - 'uv', - ['remove', 'click', '--project', 'apps/app', '-vvv'], - { - cwd: '.', + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }; + + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).toHaveBeenCalledTimes(4); + expect(spawn.sync).toHaveBeenNthCalledWith( + 1, + 'uv', + ['remove', 'numpy'], + { + cwd: 'libs/shared1', + shell: false, + stdio: 'inherit', + }, + ); + expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['sync'], { + cwd: 'libs/lib1', shell: false, stdio: 'inherit', - }, - ); - expect(output.success).toBe(true); + }); + expect(spawn.sync).toHaveBeenNthCalledWith(3, 'uv', ['sync'], { + cwd: 'apps/app', + shell: false, + stdio: 'inherit', + }); + expect(spawn.sync).toHaveBeenNthCalledWith(4, 'uv', ['sync'], { + cwd: 'apps/app1', + shell: false, + stdio: 'inherit', + }); + expect(output.success).toBe(true); + }); }); }); }); diff --git a/packages/nx-python/src/executors/remove/executor.ts b/packages/nx-python/src/executors/remove/executor.ts index 468ed5e..8d5891e 100644 --- a/packages/nx-python/src/executors/remove/executor.ts +++ b/packages/nx-python/src/executors/remove/executor.ts @@ -10,7 +10,12 @@ export default async function executor( const workspaceRoot = context.root; process.chdir(workspaceRoot); try { - const provider = await getProvider(workspaceRoot); + const provider = await getProvider( + workspaceRoot, + undefined, + undefined, + context, + ); await provider.remove(options, context); return { diff --git a/packages/nx-python/src/executors/run-commands/executor.spec.ts b/packages/nx-python/src/executors/run-commands/executor.spec.ts index 435c130..f48ee31 100644 --- a/packages/nx-python/src/executors/run-commands/executor.spec.ts +++ b/packages/nx-python/src/executors/run-commands/executor.spec.ts @@ -1,12 +1,16 @@ import { vi } from 'vitest'; - +import { vol } from 'memfs'; +import '../../utils/mocks/fs.mock'; vi.mock('nx/src/executors/run-commands/run-commands.impl'); vi.mock('../../provider/poetry/utils'); import executor from './executor'; import { ExecutorContext } from '@nx/devkit'; +import dedent from 'string-dedent'; describe('run commands executor', () => { + const originalEnv = process.env; + const context: ExecutorContext = { cwd: '', root: '.', @@ -28,6 +32,14 @@ describe('run commands executor', () => { }, }; + afterEach(() => { + vol.reset(); + }); + + beforeEach(() => { + process.env = { ...originalEnv }; + }); + describe('poetry', () => { it('should activate the venv and call the base executor', async () => { const options = { @@ -45,4 +57,61 @@ describe('run commands executor', () => { ).toHaveBeenCalledWith(options, context); }); }); + + describe('uv', () => { + describe('workspace', () => { + beforeEach(() => { + vol.fromJSON({ + 'uv.lock': '', + }); + }); + + it('should activate the venv and call the base executor', async () => { + const options = { + command: 'test', + __unparsed__: [], + }; + await executor(options, context); + + expect( + (await import('nx/src/executors/run-commands/run-commands.impl')) + .default, + ).toHaveBeenCalledWith(options, context); + + expect(process.env.PATH).toContain(`${process.cwd()}/.venv/bin`); + }); + }); + + describe('project', () => { + beforeEach(() => { + vol.fromJSON({ + 'apps/app/pyproject.toml': dedent` + [project] + name = "app" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + `, + }); + }); + + it('should activate the venv and call the base executor', async () => { + const options = { + command: 'test', + __unparsed__: [], + }; + await executor(options, context); + + expect( + (await import('nx/src/executors/run-commands/run-commands.impl')) + .default, + ).toHaveBeenCalledWith(options, context); + + expect(process.env.PATH).toContain( + `${process.cwd()}/apps/app/.venv/bin`, + ); + }); + }); + }); }); diff --git a/packages/nx-python/src/executors/run-commands/executor.ts b/packages/nx-python/src/executors/run-commands/executor.ts index d81531f..52ef7f5 100644 --- a/packages/nx-python/src/executors/run-commands/executor.ts +++ b/packages/nx-python/src/executors/run-commands/executor.ts @@ -8,7 +8,12 @@ export default async function executor( options: RunCommandsOptions, context: ExecutorContext, ) { - const provider = await getProvider(context.root); - provider.activateVenv(context.root); + const provider = await getProvider( + context.root, + undefined, + undefined, + context, + ); + provider.activateVenv(context.root, context); return baseExecutor(options, context); } diff --git a/packages/nx-python/src/executors/update/executor.spec.ts b/packages/nx-python/src/executors/update/executor.spec.ts index 04b449d..724421f 100644 --- a/packages/nx-python/src/executors/update/executor.spec.ts +++ b/packages/nx-python/src/executors/update/executor.spec.ts @@ -937,93 +937,241 @@ describe('Update Executor', () => { vi.spyOn(process, 'chdir').mockReturnValue(undefined); }); - beforeEach(() => { - vol.fromJSON({ - 'uv.lock': '', + describe('workspace', () => { + beforeEach(() => { + vol.fromJSON({ + 'uv.lock': '', + }); }); - }); - it('should return success false when the uv is not installed', async () => { - checkPrerequisites.mockRejectedValue(new Error('uv not found')); + it('should return success false when the uv is not installed', async () => { + checkPrerequisites.mockRejectedValue(new Error('uv not found')); + + const options = { + name: 'numpy', + local: false, + }; + + const context: ExecutorContext = { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + }, + }, + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }; - const options = { - name: 'numpy', - local: false, - }; + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).not.toHaveBeenCalled(); + expect(output.success).toBe(false); + }); - const context: ExecutorContext = { - cwd: '', - root: '.', - isVerbose: false, - projectName: 'app', - projectsConfigurations: { - version: 2, - projects: { - app: { - root: 'apps/app', - targets: {}, + it('run update target and should update the dependency to the project', async () => { + const options = { + name: 'numpy', + local: false, + }; + + const context: ExecutorContext = { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'app', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, }, }, - }, - nxJsonConfiguration: {}, - projectGraph: { - dependencies: {}, - nodes: {}, - }, - }; - - const output = await executor(options, context); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(spawn.sync).not.toHaveBeenCalled(); - expect(output.success).toBe(false); + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }; + + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).toHaveBeenCalledTimes(2); + expect(spawn.sync).toHaveBeenNthCalledWith( + 1, + 'uv', + ['lock', '--upgrade-package', 'numpy', '--project', 'apps/app'], + { + cwd: '.', + shell: false, + stdio: 'inherit', + }, + ); + expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['sync'], { + cwd: '.', + shell: false, + stdio: 'inherit', + }); + expect(output.success).toBe(true); + }); }); - it('run update target and should update the dependency to the project', async () => { - const options = { - name: 'numpy', - local: false, - }; - - const context: ExecutorContext = { - cwd: '', - root: '.', - isVerbose: false, - projectName: 'app', - projectsConfigurations: { - version: 2, - projects: { - app: { - root: 'apps/app', - targets: {}, + describe('project', () => { + it('run update target and should update all the dependency tree', async () => { + vol.fromJSON({ + 'apps/app/pyproject.toml': dedent` + [project] + name = "app" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "lib1", + ] + + [tool.hatch.build.targets.wheel] + packages = ["app"] + + [tool.uv.sources] + lib1 = { path = "../../libs/lib1" } + `, + + 'apps/app1/pyproject.toml': dedent` + [project] + name = "app1" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "lib1", + ] + + [tool.hatch.build.targets.wheel] + packages = ["app1"] + + [tool.uv.sources] + lib1 = { path = "../../libs/lib1" } + `, + + 'libs/lib1/pyproject.toml': dedent` + [project] + name = "lib1" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [ + "shared1", + ] + + [tool.hatch.build.targets.wheel] + packages = ["lib1"] + + [tool.uv.sources] + shared1 = { path = "../shared1" } + `, + + 'libs/shared1/pyproject.toml': dedent` + [project] + name = "shared1" + version = "0.1.0" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + + [tool.hatch.build.targets.wheel] + packages = ["shared1"] + `, + }); + + const options = { + name: 'numpy', + local: false, + }; + + const context: ExecutorContext = { + cwd: '', + root: '.', + isVerbose: false, + projectName: 'shared1', + projectsConfigurations: { + version: 2, + projects: { + app: { + root: 'apps/app', + targets: {}, + }, + app1: { + root: 'apps/app1', + targets: {}, + }, + app3: { + root: 'apps/app3', + targets: {}, + }, + lib1: { + root: 'libs/lib1', + targets: {}, + }, + shared1: { + root: 'libs/shared1', + targets: {}, + }, }, }, - }, - nxJsonConfiguration: {}, - projectGraph: { - dependencies: {}, - nodes: {}, - }, - }; - - const output = await executor(options, context); - expect(checkPrerequisites).toHaveBeenCalled(); - expect(spawn.sync).toHaveBeenCalledTimes(2); - expect(spawn.sync).toHaveBeenNthCalledWith( - 1, - 'uv', - ['lock', '--upgrade-package', 'numpy', '--project', 'apps/app'], - { - cwd: '.', + nxJsonConfiguration: {}, + projectGraph: { + dependencies: {}, + nodes: {}, + }, + }; + + const output = await executor(options, context); + expect(checkPrerequisites).toHaveBeenCalled(); + expect(spawn.sync).toHaveBeenCalledTimes(5); + expect(spawn.sync).toHaveBeenNthCalledWith( + 1, + 'uv', + ['lock', '--upgrade-package', 'numpy'], + { + cwd: 'libs/shared1', + shell: false, + stdio: 'inherit', + }, + ); + expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['sync'], { + cwd: 'libs/shared1', shell: false, stdio: 'inherit', - }, - ); - expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['sync'], { - cwd: '.', - shell: false, - stdio: 'inherit', + }); + expect(spawn.sync).toHaveBeenNthCalledWith(3, 'uv', ['sync'], { + cwd: 'libs/lib1', + shell: false, + stdio: 'inherit', + }); + expect(spawn.sync).toHaveBeenNthCalledWith(4, 'uv', ['sync'], { + cwd: 'apps/app', + shell: false, + stdio: 'inherit', + }); + expect(spawn.sync).toHaveBeenNthCalledWith(5, 'uv', ['sync'], { + cwd: 'apps/app1', + shell: false, + stdio: 'inherit', + }); + expect(output.success).toBe(true); }); - expect(output.success).toBe(true); }); }); }); diff --git a/packages/nx-python/src/executors/update/executor.ts b/packages/nx-python/src/executors/update/executor.ts index 99f5bab..f1842e4 100644 --- a/packages/nx-python/src/executors/update/executor.ts +++ b/packages/nx-python/src/executors/update/executor.ts @@ -11,7 +11,12 @@ export default async function executor( process.chdir(workspaceRoot); try { - const provider = await getProvider(workspaceRoot); + const provider = await getProvider( + workspaceRoot, + undefined, + undefined, + context, + ); await provider.update(options, context); return { diff --git a/packages/nx-python/src/generators/migrate-to-shared-venv/__snapshots__/generator.spec.ts.snap b/packages/nx-python/src/generators/migrate-to-shared-venv/__snapshots__/generator.spec.ts.snap index 0574c9e..99b62fb 100644 --- a/packages/nx-python/src/generators/migrate-to-shared-venv/__snapshots__/generator.spec.ts.snap +++ b/packages/nx-python/src/generators/migrate-to-shared-venv/__snapshots__/generator.spec.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`nx-python migrate-shared-venv generator > should migrate an isolate venv to shared venv 1`] = ` +exports[`nx-python migrate-shared-venv generator > poetry > should migrate an isolate venv to shared venv 1`] = ` "[tool.coverage.run] branch = true source = [ "proj1" ] @@ -33,7 +33,7 @@ build-backend = "poetry.core.masonry.api" " `; -exports[`nx-python migrate-shared-venv generator > should migrate an isolate venv to shared venv 2`] = ` +exports[`nx-python migrate-shared-venv generator > poetry > should migrate an isolate venv to shared venv 2`] = ` "[tool.poetry] name = "@nxlv/nx-plugins" version = "1.0.0" @@ -72,12 +72,12 @@ build-backend = "poetry.core.masonry.api" " `; -exports[`nx-python migrate-shared-venv generator > should migrate an isolate venv to shared venv 3`] = ` +exports[`nx-python migrate-shared-venv generator > poetry > should migrate an isolate venv to shared venv 3`] = ` "3.8.11 " `; -exports[`nx-python migrate-shared-venv generator > should migrate an isolate venv to shared venv project without dev dependencies 1`] = ` +exports[`nx-python migrate-shared-venv generator > poetry > should migrate an isolate venv to shared venv project without dev dependencies 1`] = ` "[tool.coverage.run] branch = true source = [ "proj1" ] @@ -109,7 +109,7 @@ build-backend = "poetry.core.masonry.api" " `; -exports[`nx-python migrate-shared-venv generator > should migrate an isolate venv to shared venv project without dev dependencies 2`] = ` +exports[`nx-python migrate-shared-venv generator > poetry > should migrate an isolate venv to shared venv project without dev dependencies 2`] = ` "[tool.poetry] name = "@nxlv/nx-plugins" version = "1.0.0" @@ -132,12 +132,12 @@ build-backend = "poetry.core.masonry.api" " `; -exports[`nx-python migrate-shared-venv generator > should migrate an isolate venv to shared venv project without dev dependencies 3`] = ` +exports[`nx-python migrate-shared-venv generator > poetry > should migrate an isolate venv to shared venv project without dev dependencies 3`] = ` "3.8.11 " `; -exports[`nx-python migrate-shared-venv generator > should migrate an isolate venv to shared venv with auto activate enabled 1`] = ` +exports[`nx-python migrate-shared-venv generator > poetry > should migrate an isolate venv to shared venv with auto activate enabled 1`] = ` "[tool.nx] autoActivate = true @@ -178,3 +178,118 @@ requires = [ "poetry-core==1.1.0" ] build-backend = "poetry.core.masonry.api" " `; + +exports[`nx-python migrate-shared-venv generator > uv > should migrate an isolate venv to shared venv 1`] = ` +"[tool.coverage.run] +branch = true +source = [ "proj1" ] + +[tool.coverage.report] +exclude_lines = [ "if TYPE_CHECKING:" ] +show_missing = true + +[tool.pytest.ini_options] +addopts = "--cov --cov-report html:'../../coverage/apps/proj1/html' --cov-report xml:'../../coverage/apps/proj1/coverage.xml' --html='../../reports/apps/proj1/unittests/html/index.html' --junitxml='../../reports/apps/proj1/unittests/junit.xml'" + +[tool.hatch.build.targets.wheel] +packages = [ "proj1" ] + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.ruff] +exclude = [ ".ruff_cache", ".svn", ".tox", ".venv", "dist" ] +line-length = 88 +indent-width = 4 + + [tool.ruff.lint] + select = [ "E", "F", "UP", "B", "SIM", "I" ] + ignore = [ ] + fixable = [ "ALL" ] + unfixable = [ ] + +[project] +name = "proj1" +version = "1.0.0" +description = "Automatically generated by Nx." +requires-python = ">=3.9,<4" +readme = "README.md" +dependencies = [ ] + +[build-system] +requires = [ "hatchling" ] +build-backend = "hatchling.build" +" +`; + +exports[`nx-python migrate-shared-venv generator > uv > should migrate an isolate venv to shared venv 2`] = ` +"[project] +name = "nx-workspace" +version = "1.0.0" +dependencies = [ "proj1" ] + +[tool.uv.workspace] +members = [ "apps/proj1" ] + +[tool.uv.sources.proj1] +workspace = true + +[dependency-groups] +dev = [ + "autopep8>=2.3.1", + "ruff>=0.8.2", + "pytest>=8.3.4", + "pytest-sugar>=1.0.0", + "pytest-cov>=6.0.0", + "pytest-html>=4.1.1" +] +" +`; + +exports[`nx-python migrate-shared-venv generator > uv > should migrate an isolate venv to shared venv 3`] = ` +"3.8.11 +" +`; + +exports[`nx-python migrate-shared-venv generator > uv > should migrate an isolate venv to shared venv project without dev dependencies 1`] = ` +"[project] +name = "proj1" +version = "1.0.0" +description = "Automatically generated by Nx." +requires-python = ">=3.9,<4" +readme = "README.md" +dependencies = [ ] + +[tool.hatch.build.targets.wheel] +packages = [ "proj1" ] + +[tool.hatch.metadata] +allow-direct-references = true + +[build-system] +requires = [ "hatchling" ] +build-backend = "hatchling.build" +" +`; + +exports[`nx-python migrate-shared-venv generator > uv > should migrate an isolate venv to shared venv project without dev dependencies 2`] = ` +"[project] +name = "nx-workspace" +version = "1.0.0" +dependencies = [ "proj1" ] + +[tool.uv.workspace] +members = [ "apps/proj1" ] + +[tool.uv.sources.proj1] +workspace = true + +[dependency-groups] +dev = [ "autopep8>=2.3.1" ] +" +`; + +exports[`nx-python migrate-shared-venv generator > uv > should migrate an isolate venv to shared venv project without dev dependencies 3`] = ` +"3.8.11 +" +`; diff --git a/packages/nx-python/src/generators/migrate-to-shared-venv/files/__dot__python-version.template b/packages/nx-python/src/generators/migrate-to-shared-venv/files/base/__dot__python-version.template similarity index 100% rename from packages/nx-python/src/generators/migrate-to-shared-venv/files/__dot__python-version.template rename to packages/nx-python/src/generators/migrate-to-shared-venv/files/base/__dot__python-version.template diff --git a/packages/nx-python/src/generators/migrate-to-shared-venv/files/poetry.toml b/packages/nx-python/src/generators/migrate-to-shared-venv/files/poetry/poetry.toml similarity index 100% rename from packages/nx-python/src/generators/migrate-to-shared-venv/files/poetry.toml rename to packages/nx-python/src/generators/migrate-to-shared-venv/files/poetry/poetry.toml diff --git a/packages/nx-python/src/generators/migrate-to-shared-venv/files/pyproject.toml b/packages/nx-python/src/generators/migrate-to-shared-venv/files/poetry/pyproject.toml similarity index 100% rename from packages/nx-python/src/generators/migrate-to-shared-venv/files/pyproject.toml rename to packages/nx-python/src/generators/migrate-to-shared-venv/files/poetry/pyproject.toml diff --git a/packages/nx-python/src/generators/migrate-to-shared-venv/files/uv/pyproject.toml b/packages/nx-python/src/generators/migrate-to-shared-venv/files/uv/pyproject.toml new file mode 100644 index 0000000..b3eca3f --- /dev/null +++ b/packages/nx-python/src/generators/migrate-to-shared-venv/files/uv/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "nx-workspace" +version = "1.0.0" +dependencies = [] + +[tool.uv.workspace] +members = [] diff --git a/packages/nx-python/src/generators/migrate-to-shared-venv/generator.spec.ts b/packages/nx-python/src/generators/migrate-to-shared-venv/generator.spec.ts index 677d679..ca8aaf7 100644 --- a/packages/nx-python/src/generators/migrate-to-shared-venv/generator.spec.ts +++ b/packages/nx-python/src/generators/migrate-to-shared-venv/generator.spec.ts @@ -1,156 +1,299 @@ import { vi, MockInstance } from 'vitest'; import '../../utils/mocks/cross-spawn.mock'; import * as poetryUtils from '../../provider/poetry/utils'; +import * as uvUtils from '../../provider/uv/utils'; import { Tree } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import generator from './generator'; -import projectGenerator from '../project/generator'; +import poetryProjectGenerator from '../project/generator'; +import uvProjectGenerator from '../uv-project/generator'; import spawn from 'cross-spawn'; describe('nx-python migrate-shared-venv generator', () => { - let checkPoetryExecutableMock: MockInstance; let appTree: Tree; beforeEach(() => { + vi.resetAllMocks(); + appTree = createTreeWithEmptyWorkspace({ layout: 'apps-libs', }); - checkPoetryExecutableMock = vi.spyOn(poetryUtils, 'checkPoetryExecutable'); - checkPoetryExecutableMock.mockResolvedValue(undefined); - vi.mocked(spawn.sync).mockReturnValue({ - status: 0, - output: [''], - pid: 0, - signal: null, - stderr: null, - stdout: null, + vi.mocked(spawn.sync).mockImplementation((command) => { + if (command === 'python') { + return { + status: 0, + output: [''], + pid: 0, + signal: null, + stderr: null, + stdout: Buffer.from('Python 3.8.11'), + }; + } + + return { + status: 0, + output: [''], + pid: 0, + signal: null, + stderr: null, + stdout: null, + }; }); }); - it('should throw an exception when the poetry is not installed', async () => { - checkPoetryExecutableMock.mockRejectedValue(new Error('poetry not found')); + describe('poetry', () => { + let checkPoetryExecutableMock: MockInstance; + + beforeEach(() => { + checkPoetryExecutableMock = vi.spyOn( + poetryUtils, + 'checkPoetryExecutable', + ); + checkPoetryExecutableMock.mockResolvedValue(undefined); + }); + + it('should throw an exception when the poetry is not installed', async () => { + checkPoetryExecutableMock.mockRejectedValue( + new Error('poetry not found'), + ); + + expect( + generator(appTree, { + moveDevDependencies: true, + pyenvPythonVersion: '3.8.11', + pyprojectPythonDependency: '>=3.8,<3.10', + autoActivate: false, + packageManager: 'poetry', + }), + ).rejects.toThrow('poetry not found'); - expect( - generator(appTree, { + expect(checkPoetryExecutableMock).toHaveBeenCalled(); + }); + + it('should migrate an isolate venv to shared venv', async () => { + await poetryProjectGenerator(appTree, { + name: 'proj1', + type: 'application', + publishable: true, + customSource: false, + addDevDependencies: true, + moduleName: 'proj1', + packageName: 'proj1', + buildLockedVersions: true, + buildBundleLocalDependencies: true, + pyenvPythonVersion: '3.8.11', + pyprojectPythonDependency: '>=3.8,<3.10', + toxEnvlist: 'py38', + }); + + const task = await generator(appTree, { moveDevDependencies: true, pyenvPythonVersion: '3.8.11', pyprojectPythonDependency: '>=3.8,<3.10', autoActivate: false, - }), - ).rejects.toThrow('poetry not found'); - - expect(checkPoetryExecutableMock).toHaveBeenCalled(); - }); + packageManager: 'poetry', + }); + task(); - it('should migrate an isolate venv to shared venv', async () => { - await projectGenerator(appTree, { - name: 'proj1', - type: 'application', - publishable: true, - customSource: false, - addDevDependencies: true, - moduleName: 'proj1', - packageName: 'proj1', - buildLockedVersions: true, - buildBundleLocalDependencies: true, - pyenvPythonVersion: '3.8.11', - pyprojectPythonDependency: '>=3.8,<3.10', - toxEnvlist: 'py38', + expect(checkPoetryExecutableMock).toHaveBeenCalled(); + expect( + appTree.read('apps/proj1/pyproject.toml', 'utf-8'), + ).toMatchSnapshot(); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); + expect(appTree.read('.python-version', 'utf-8')).toMatchSnapshot(); + expect(spawn.sync).toHaveBeenNthCalledWith( + 1, + 'poetry', + ['lock', '--no-update'], + { cwd: 'apps/proj1', shell: false, stdio: 'inherit' }, + ); + expect(spawn.sync).toHaveBeenNthCalledWith(2, 'poetry', ['install'], { + shell: false, + stdio: 'inherit', + }); }); - const task = await generator(appTree, { - moveDevDependencies: true, - pyenvPythonVersion: '3.8.11', - pyprojectPythonDependency: '>=3.8,<3.10', - autoActivate: false, + it('should migrate an isolate venv to shared venv project without dev dependencies', async () => { + await poetryProjectGenerator(appTree, { + name: 'proj1', + type: 'application', + publishable: true, + customSource: false, + addDevDependencies: false, + moduleName: 'proj1', + packageName: 'proj1', + buildLockedVersions: true, + buildBundleLocalDependencies: true, + pyenvPythonVersion: '3.8.11', + pyprojectPythonDependency: '>=3.8,<3.10', + toxEnvlist: 'py38', + }); + + const task = await generator(appTree, { + moveDevDependencies: true, + pyenvPythonVersion: '3.8.11', + pyprojectPythonDependency: '>=3.8,<3.10', + autoActivate: false, + packageManager: 'poetry', + }); + task(); + + expect(checkPoetryExecutableMock).toHaveBeenCalled(); + expect( + appTree.read('apps/proj1/pyproject.toml', 'utf-8'), + ).toMatchSnapshot(); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); + expect(appTree.read('.python-version', 'utf-8')).toMatchSnapshot(); + expect(spawn.sync).toHaveBeenNthCalledWith( + 1, + 'poetry', + ['lock', '--no-update'], + { cwd: 'apps/proj1', shell: false, stdio: 'inherit' }, + ); + expect(spawn.sync).toHaveBeenNthCalledWith(2, 'poetry', ['install'], { + shell: false, + stdio: 'inherit', + }); }); - task(); - - expect(checkPoetryExecutableMock).toHaveBeenCalled(); - expect( - appTree.read('apps/proj1/pyproject.toml', 'utf-8'), - ).toMatchSnapshot(); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - expect(appTree.read('.python-version', 'utf-8')).toMatchSnapshot(); - expect(spawn.sync).toHaveBeenNthCalledWith( - 1, - 'poetry', - ['lock', '--no-update'], - { cwd: 'apps/proj1', shell: false, stdio: 'inherit' }, - ); - expect(spawn.sync).toHaveBeenNthCalledWith(2, 'poetry', ['install'], { - shell: false, - stdio: 'inherit', + + it('should migrate an isolate venv to shared venv with auto activate enabled', async () => { + await poetryProjectGenerator(appTree, { + name: 'proj1', + type: 'application', + publishable: true, + customSource: false, + addDevDependencies: true, + moduleName: 'proj1', + packageName: 'proj1', + buildLockedVersions: true, + buildBundleLocalDependencies: true, + pyenvPythonVersion: '3.8.11', + pyprojectPythonDependency: '>=3.8,<3.10', + toxEnvlist: 'py38', + }); + + const task = await generator(appTree, { + moveDevDependencies: true, + pyenvPythonVersion: '3.8.11', + pyprojectPythonDependency: '>=3.8,<3.10', + autoActivate: true, + packageManager: 'poetry', + }); + task(); + + expect(checkPoetryExecutableMock).toHaveBeenCalled(); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); }); }); - it('should migrate an isolate venv to shared venv project without dev dependencies', async () => { - await projectGenerator(appTree, { - name: 'proj1', - type: 'application', - publishable: true, - customSource: false, - addDevDependencies: false, - moduleName: 'proj1', - packageName: 'proj1', - buildLockedVersions: true, - buildBundleLocalDependencies: true, - pyenvPythonVersion: '3.8.11', - pyprojectPythonDependency: '>=3.8,<3.10', - toxEnvlist: 'py38', - }); + describe('uv', () => { + let checkUvExecutableMock: MockInstance; - const task = await generator(appTree, { - moveDevDependencies: true, - pyenvPythonVersion: '3.8.11', - pyprojectPythonDependency: '>=3.8,<3.10', - autoActivate: false, + beforeEach(() => { + checkUvExecutableMock = vi.spyOn(uvUtils, 'checkUvExecutable'); + checkUvExecutableMock.mockResolvedValue(undefined); }); - task(); - - expect(checkPoetryExecutableMock).toHaveBeenCalled(); - expect( - appTree.read('apps/proj1/pyproject.toml', 'utf-8'), - ).toMatchSnapshot(); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - expect(appTree.read('.python-version', 'utf-8')).toMatchSnapshot(); - expect(spawn.sync).toHaveBeenNthCalledWith( - 1, - 'poetry', - ['lock', '--no-update'], - { cwd: 'apps/proj1', shell: false, stdio: 'inherit' }, - ); - expect(spawn.sync).toHaveBeenNthCalledWith(2, 'poetry', ['install'], { - shell: false, - stdio: 'inherit', - }); - }); - it('should migrate an isolate venv to shared venv with auto activate enabled', async () => { - await projectGenerator(appTree, { - name: 'proj1', - type: 'application', - publishable: true, - customSource: false, - addDevDependencies: true, - moduleName: 'proj1', - packageName: 'proj1', - buildLockedVersions: true, - buildBundleLocalDependencies: true, - pyenvPythonVersion: '3.8.11', - pyprojectPythonDependency: '>=3.8,<3.10', - toxEnvlist: 'py38', + it('should throw an exception when the uv is not installed', async () => { + checkUvExecutableMock.mockRejectedValue(new Error('uv not found')); + + expect( + generator(appTree, { + moveDevDependencies: true, + pyenvPythonVersion: '3.8.11', + pyprojectPythonDependency: '>=3.8,<3.10', + autoActivate: false, + packageManager: 'uv', + }), + ).rejects.toThrow('uv not found'); + + expect(checkUvExecutableMock).toHaveBeenCalled(); }); - const task = await generator(appTree, { - moveDevDependencies: true, - pyenvPythonVersion: '3.8.11', - pyprojectPythonDependency: '>=3.8,<3.10', - autoActivate: true, + it('should migrate an isolate venv to shared venv', async () => { + await uvProjectGenerator(appTree, { + name: 'proj1', + projectType: 'application', + pyprojectPythonDependency: '', + publishable: false, + buildLockedVersions: false, + buildBundleLocalDependencies: false, + linter: 'ruff', + unitTestRunner: 'pytest', + rootPyprojectDependencyGroup: 'main', + unitTestHtmlReport: true, + unitTestJUnitReport: true, + codeCoverage: true, + codeCoverageHtmlReport: true, + codeCoverageXmlReport: true, + projectNameAndRootFormat: 'derived', + }); + + const task = await generator(appTree, { + moveDevDependencies: true, + pyenvPythonVersion: '3.8.11', + pyprojectPythonDependency: '>=3.8,<3.10', + autoActivate: false, + packageManager: 'uv', + }); + task(); + + expect(checkUvExecutableMock).toHaveBeenCalled(); + expect( + appTree.read('apps/proj1/pyproject.toml', 'utf-8'), + ).toMatchSnapshot(); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); + expect(appTree.read('.python-version', 'utf-8')).toMatchSnapshot(); + expect(spawn.sync).toHaveBeenNthCalledWith(1, 'python', ['--version'], { + stdio: 'pipe', + }); + expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['sync'], { + shell: false, + stdio: 'inherit', + }); }); - task(); - expect(checkPoetryExecutableMock).toHaveBeenCalled(); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); + it('should migrate an isolate venv to shared venv project without dev dependencies', async () => { + await uvProjectGenerator(appTree, { + name: 'proj1', + projectType: 'application', + pyprojectPythonDependency: '', + publishable: false, + buildLockedVersions: false, + buildBundleLocalDependencies: false, + linter: 'none', + unitTestRunner: 'none', + rootPyprojectDependencyGroup: 'main', + unitTestHtmlReport: false, + unitTestJUnitReport: false, + codeCoverage: false, + codeCoverageHtmlReport: false, + codeCoverageXmlReport: false, + projectNameAndRootFormat: 'derived', + }); + + const task = await generator(appTree, { + moveDevDependencies: true, + pyenvPythonVersion: '3.8.11', + pyprojectPythonDependency: '>=3.8,<3.10', + autoActivate: false, + packageManager: 'uv', + }); + task(); + + expect(checkUvExecutableMock).toHaveBeenCalled(); + expect( + appTree.read('apps/proj1/pyproject.toml', 'utf-8'), + ).toMatchSnapshot(); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); + expect(appTree.read('.python-version', 'utf-8')).toMatchSnapshot(); + expect(spawn.sync).toHaveBeenNthCalledWith(1, 'python', ['--version'], { + stdio: 'pipe', + }); + expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['sync'], { + shell: false, + stdio: 'inherit', + }); + }); }); }); diff --git a/packages/nx-python/src/generators/migrate-to-shared-venv/generator.ts b/packages/nx-python/src/generators/migrate-to-shared-venv/generator.ts index 0b69386..1aeba30 100644 --- a/packages/nx-python/src/generators/migrate-to-shared-venv/generator.ts +++ b/packages/nx-python/src/generators/migrate-to-shared-venv/generator.ts @@ -12,6 +12,8 @@ import { parse, stringify } from '@iarna/toml'; import chalk from 'chalk'; import { PoetryPyprojectToml } from '../../provider/poetry'; import { checkPoetryExecutable, runPoetry } from '../../provider/poetry/utils'; +import { checkUvExecutable, runUv } from '../../provider/uv/utils'; +import { UVPyprojectToml } from '../../provider/uv/types'; async function addFiles(host: Tree, options: Schema) { const packageJson = await readJsonFile('package.json'); @@ -24,12 +26,36 @@ async function addFiles(host: Tree, options: Schema) { description: packageJson.description || '', }; - generateFiles(host, path.join(__dirname, 'files'), '.', templateOptions); + generateFiles( + host, + path.join(__dirname, 'files', 'base'), + '.', + templateOptions, + ); + + if (options.packageManager === 'poetry') { + generateFiles( + host, + path.join(__dirname, 'files', 'poetry'), + '.', + templateOptions, + ); + } else if (options.packageManager === 'uv') { + generateFiles( + host, + path.join(__dirname, 'files', 'uv'), + '.', + templateOptions, + ); + } } type LockUpdateTask = () => void; -function updatePyprojectRoot(host: Tree, options: Schema): LockUpdateTask[] { +function updatePoetryPyprojectRoot( + host: Tree, + options: Schema, +): LockUpdateTask[] { const postGeneratorTasks = []; const rootPyprojectToml = parse( @@ -52,7 +78,7 @@ function updatePyprojectRoot(host: Tree, options: Schema): LockUpdateTask[] { }; if (options.moveDevDependencies) { postGeneratorTasks.push( - moveDevDependencies( + movePoetryDevDependencies( pyprojectToml, rootPyprojectToml, host, @@ -69,7 +95,7 @@ function updatePyprojectRoot(host: Tree, options: Schema): LockUpdateTask[] { return postGeneratorTasks; } -function moveDevDependencies( +function movePoetryDevDependencies( pyprojectToml: PoetryPyprojectToml, rootPyprojectToml: PoetryPyprojectToml, host: Tree, @@ -103,28 +129,116 @@ function moveDevDependencies( }; } -function updateRootPoetryLock() { - console.log(chalk` Updating root {bgBlue poetry.lock}...`); - runPoetry(['install'], { log: false }); - console.log(chalk`\n {bgBlue poetry.lock} updated.\n`); +function updateUvPyprojectRoot(host: Tree, options: Schema): LockUpdateTask[] { + const postGeneratorTasks = []; + + const rootPyprojectToml = parse( + host.read('pyproject.toml').toString(), + ) as UVPyprojectToml; + + for (const project of getProjects(host)) { + const [, projectConfig] = project; + const pyprojectTomlPath = path.join(projectConfig.root, 'pyproject.toml'); + if (host.exists(pyprojectTomlPath)) { + const pyprojectToml = parse( + host.read(pyprojectTomlPath).toString(), + ) as UVPyprojectToml; + + rootPyprojectToml.project.dependencies.push(pyprojectToml.project.name); + rootPyprojectToml.tool ??= {}; + rootPyprojectToml.tool.uv ??= {}; + rootPyprojectToml.tool.uv.sources ??= {}; + rootPyprojectToml.tool.uv.sources[pyprojectToml.project.name] = { + workspace: true, + }; + rootPyprojectToml.tool.uv.workspace ??= { + members: [], + }; + rootPyprojectToml.tool.uv.workspace.members.push(projectConfig.root); + + for (const source of Object.keys(pyprojectToml.tool?.uv?.sources ?? {})) { + if (pyprojectToml.tool.uv.sources[source].path) { + pyprojectToml.tool.uv.sources[source] = { workspace: true }; + } + } + + if (options.moveDevDependencies) { + moveUvDevDependencies(pyprojectToml, rootPyprojectToml); + } + + host.write(pyprojectTomlPath, stringify(pyprojectToml)); + host.delete(path.join(projectConfig.root, 'uv.lock')); + host.delete(path.join(projectConfig.root, '.venv')); + } + } + + host.write('pyproject.toml', stringify(rootPyprojectToml)); + + return postGeneratorTasks; +} + +function moveUvDevDependencies( + pyprojectToml: UVPyprojectToml, + rootPyprojectToml: UVPyprojectToml, +) { + const devDependencies = pyprojectToml?.['dependency-groups']?.dev || []; + rootPyprojectToml['dependency-groups'] ??= {}; + rootPyprojectToml['dependency-groups'].dev ??= []; + + for (const devDependency of devDependencies) { + if ( + rootPyprojectToml['dependency-groups'].dev.some( + (dep) => + /^[a-zA-Z0-9-]+/.exec(dep)?.[0] === + /^[a-zA-Z0-9-]+/.exec(devDependency)?.[0], + ) + ) { + continue; + } + rootPyprojectToml['dependency-groups'].dev.push(devDependency); + } + + if (pyprojectToml['dependency-groups']?.dev?.length) { + delete pyprojectToml['dependency-groups'].dev; + } + + if (Object.keys(pyprojectToml['dependency-groups'] || {}).length === 0) { + delete pyprojectToml['dependency-groups']; + } } -async function generator(host: Tree, options: Schema) { - if (host.exists('uv.lock')) { - throw new Error( - 'Uv project detected, this generator is only for poetry projects.', - ); +function updateRootLock(options: Schema) { + if (options.packageManager === 'poetry') { + console.log(chalk` Updating root {bgBlue poetry.lock}...`); + runPoetry(['install'], { log: false }); + console.log(chalk`\n {bgBlue poetry.lock} updated.\n`); + } else if (options.packageManager === 'uv') { + console.log(chalk` Updating root {bgBlue uv.lock}...`); + runUv(['sync'], { log: false }); + console.log(chalk`\n {bgBlue uv.lock} updated.\n`); } +} - await checkPoetryExecutable(); +async function generator(host: Tree, options: Schema) { + if (options.packageManager === 'poetry') { + await checkPoetryExecutable(); + } else if (options.packageManager === 'uv') { + await checkUvExecutable(); + } await addFiles(host, options); - const lockUpdateTasks = updatePyprojectRoot(host, options); + const lockUpdateTasks: LockUpdateTask[] = []; + if (options.packageManager === 'poetry') { + lockUpdateTasks.push(...updatePoetryPyprojectRoot(host, options)); + } else if (options.packageManager === 'uv') { + lockUpdateTasks.push(...updateUvPyprojectRoot(host, options)); + } + await formatFiles(host); return () => { lockUpdateTasks.forEach((task) => task()); - updateRootPoetryLock(); + updateRootLock(options); }; } diff --git a/packages/nx-python/src/generators/migrate-to-shared-venv/schema.d.ts b/packages/nx-python/src/generators/migrate-to-shared-venv/schema.d.ts index db129e9..84af3f2 100644 --- a/packages/nx-python/src/generators/migrate-to-shared-venv/schema.d.ts +++ b/packages/nx-python/src/generators/migrate-to-shared-venv/schema.d.ts @@ -3,4 +3,5 @@ export interface Schema { pyprojectPythonDependency: string; pyenvPythonVersion: string; autoActivate: boolean; + packageManager: 'poetry' | 'uv'; } diff --git a/packages/nx-python/src/generators/migrate-to-shared-venv/schema.json b/packages/nx-python/src/generators/migrate-to-shared-venv/schema.json index cb49a39..6e7cb04 100644 --- a/packages/nx-python/src/generators/migrate-to-shared-venv/schema.json +++ b/packages/nx-python/src/generators/migrate-to-shared-venv/schema.json @@ -23,6 +23,12 @@ "type": "boolean", "description": "Specifies if the root pyproject toml should be automatically activated when running @nxlv/python executors", "default": true + }, + "packageManager": { + "type": "string", + "enum": ["poetry", "uv"], + "description": "Existing projects package manager", + "default": "poetry" } }, "required": [] diff --git a/packages/nx-python/src/generators/uv-project/__snapshots__/generator.spec.ts.snap b/packages/nx-python/src/generators/uv-project/__snapshots__/generator.spec.ts.snap index ade2e1b..d47767a 100644 --- a/packages/nx-python/src/generators/uv-project/__snapshots__/generator.spec.ts.snap +++ b/packages/nx-python/src/generators/uv-project/__snapshots__/generator.spec.ts.snap @@ -30,6 +30,15 @@ exports[`application generator > as-provided > should run successfully minimal c "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, "lock": { "executor": "@nxlv/python:run-commands", "options": { @@ -68,6 +77,14 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["my_app_test"] +[tool.hatch.metadata] +allow-direct-references = true + +[dependency-groups] +dev = [ + "autopep8>=2.3.1", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" @@ -119,6 +136,15 @@ exports[`application generator > as-provided > should run successfully minimal c "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, "lock": { "executor": "@nxlv/python:run-commands", "options": { @@ -157,6 +183,14 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["my_app_test"] +[tool.hatch.metadata] +allow-direct-references = true + +[dependency-groups] +dev = [ + "autopep8>=2.3.1", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" @@ -208,6 +242,15 @@ exports[`application generator > custom template dir > should run successfully w "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, "lock": { "executor": "@nxlv/python:run-commands", "options": { @@ -250,7 +293,7 @@ build-backend = "hatchling.build" exports[`application generator > custom template dir > should run successfully with custom template dir 3`] = `null`; -exports[`application generator > should run successfully minimal configuration as a library 1`] = ` +exports[`application generator > project > should run successfully minimal configuration as a library 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", @@ -280,6 +323,15 @@ exports[`application generator > should run successfully minimal configuration a "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, "lock": { "executor": "@nxlv/python:run-commands", "options": { @@ -299,14 +351,14 @@ exports[`application generator > should run successfully minimal configuration a } `; -exports[`application generator > should run successfully minimal configuration as a library 2`] = ` +exports[`application generator > project > should run successfully minimal configuration as a library 2`] = ` "# test Project description here. " `; -exports[`application generator > should run successfully minimal configuration as a library 3`] = ` +exports[`application generator > project > should run successfully minimal configuration as a library 3`] = ` "[project] name = "test" version = "1.0.0" @@ -318,13 +370,21 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] +[tool.hatch.metadata] +allow-direct-references = true + +[dependency-groups] +dev = [ + "autopep8>=2.3.1", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" " `; -exports[`application generator > should run successfully minimal configuration as a library 4`] = ` +exports[`application generator > project > should run successfully minimal configuration as a library 4`] = ` """"Sample Hello World application.""" @@ -334,29 +394,14 @@ def hello(): " `; -exports[`application generator > should run successfully minimal configuration as a library 5`] = ` +exports[`application generator > project > should run successfully minimal configuration as a library 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully minimal configuration as a library 6`] = ` -"[project] -name = "nx-workspace" -version = "1.0.0" -dependencies = [ "test" ] - -[dependency-groups] -dev = [ "autopep8>=2.3.1" ] - -[tool.uv.sources.test] -workspace = true - -[tool.uv.workspace] -members = [ "libs/*" ] -" -`; +exports[`application generator > project > should run successfully minimal configuration as a library 6`] = `null`; -exports[`application generator > should run successfully minimal configuration custom directory 1`] = ` +exports[`application generator > project > should run successfully minimal configuration custom directory 1`] = ` { "$schema": "../../../node_modules/nx/schemas/project-schema.json", "name": "subdir-test", @@ -386,6 +431,15 @@ exports[`application generator > should run successfully minimal configuration c "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, "lock": { "executor": "@nxlv/python:run-commands", "options": { @@ -405,14 +459,14 @@ exports[`application generator > should run successfully minimal configuration c } `; -exports[`application generator > should run successfully minimal configuration custom directory 2`] = ` +exports[`application generator > project > should run successfully minimal configuration custom directory 2`] = ` "# subdir-test Project description here. " `; -exports[`application generator > should run successfully minimal configuration custom directory 3`] = ` +exports[`application generator > project > should run successfully minimal configuration custom directory 3`] = ` "[project] name = "subdir-test" version = "1.0.0" @@ -424,13 +478,21 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["subdir_test"] +[tool.hatch.metadata] +allow-direct-references = true + +[dependency-groups] +dev = [ + "autopep8>=2.3.1", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" " `; -exports[`application generator > should run successfully minimal configuration custom directory 4`] = ` +exports[`application generator > project > should run successfully minimal configuration custom directory 4`] = ` """"Sample Hello World application.""" @@ -440,29 +502,14 @@ def hello(): " `; -exports[`application generator > should run successfully minimal configuration custom directory 5`] = ` +exports[`application generator > project > should run successfully minimal configuration custom directory 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully minimal configuration custom directory 6`] = ` -"[project] -name = "nx-workspace" -version = "1.0.0" -dependencies = [ "subdir-test" ] - -[dependency-groups] -dev = [ "autopep8>=2.3.1" ] - -[tool.uv.sources.subdir-test] -workspace = true - -[tool.uv.workspace] -members = [ "apps/*" ] -" -`; +exports[`application generator > project > should run successfully minimal configuration custom directory 6`] = `null`; -exports[`application generator > should run successfully minimal configuration with tags 1`] = ` +exports[`application generator > project > should run successfully minimal configuration with tags 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", @@ -495,6 +542,15 @@ exports[`application generator > should run successfully minimal configuration w "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, "lock": { "executor": "@nxlv/python:run-commands", "options": { @@ -514,14 +570,14 @@ exports[`application generator > should run successfully minimal configuration w } `; -exports[`application generator > should run successfully minimal configuration with tags 2`] = ` +exports[`application generator > project > should run successfully minimal configuration with tags 2`] = ` "# test Project description here. " `; -exports[`application generator > should run successfully minimal configuration with tags 3`] = ` +exports[`application generator > project > should run successfully minimal configuration with tags 3`] = ` "[project] name = "test" version = "1.0.0" @@ -533,13 +589,21 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] +[tool.hatch.metadata] +allow-direct-references = true + +[dependency-groups] +dev = [ + "autopep8>=2.3.1", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" " `; -exports[`application generator > should run successfully minimal configuration with tags 4`] = ` +exports[`application generator > project > should run successfully minimal configuration with tags 4`] = ` """"Sample Hello World application.""" @@ -549,29 +613,14 @@ def hello(): " `; -exports[`application generator > should run successfully minimal configuration with tags 5`] = ` +exports[`application generator > project > should run successfully minimal configuration with tags 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully minimal configuration with tags 6`] = ` -"[project] -name = "nx-workspace" -version = "1.0.0" -dependencies = [ "test" ] - -[dependency-groups] -dev = [ "autopep8>=2.3.1" ] - -[tool.uv.sources.test] -workspace = true - -[tool.uv.workspace] -members = [ "apps/*" ] -" -`; +exports[`application generator > project > should run successfully minimal configuration with tags 6`] = `null`; -exports[`application generator > should run successfully with flake8 linter 1`] = ` +exports[`application generator > project > should run successfully with flake8 linter 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", @@ -601,6 +650,15 @@ exports[`application generator > should run successfully with flake8 linter 1`] "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, "lint": { "executor": "@nxlv/python:flake8", "options": { @@ -629,14 +687,14 @@ exports[`application generator > should run successfully with flake8 linter 1`] } `; -exports[`application generator > should run successfully with flake8 linter 2`] = ` +exports[`application generator > project > should run successfully with flake8 linter 2`] = ` "# test Project description here. " `; -exports[`application generator > should run successfully with flake8 linter 3`] = ` +exports[`application generator > project > should run successfully with flake8 linter 3`] = ` "[project] name = "test" version = "1.0.0" @@ -648,13 +706,22 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] +[tool.hatch.metadata] +allow-direct-references = true + +[dependency-groups] +dev = [ + "autopep8>=2.3.1", + "flake8>=7.1.1", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" " `; -exports[`application generator > should run successfully with flake8 linter 4`] = ` +exports[`application generator > project > should run successfully with flake8 linter 4`] = ` """"Sample Hello World application.""" @@ -664,12 +731,12 @@ def hello(): " `; -exports[`application generator > should run successfully with flake8 linter 5`] = ` +exports[`application generator > project > should run successfully with flake8 linter 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with flake8 linter 6`] = ` +exports[`application generator > project > should run successfully with flake8 linter 6`] = ` "[flake8] exclude = .git, @@ -684,24 +751,9 @@ max-line-length = 120 " `; -exports[`application generator > should run successfully with flake8 linter 7`] = ` -"[project] -name = "nx-workspace" -version = "1.0.0" -dependencies = [ "test" ] - -[dependency-groups] -dev = [ "flake8>=7.1.1", "autopep8>=2.3.1" ] - -[tool.uv.sources.test] -workspace = true - -[tool.uv.workspace] -members = [ "apps/*" ] -" -`; +exports[`application generator > project > should run successfully with flake8 linter 7`] = `null`; -exports[`application generator > should run successfully with flake8 linter and pytest with html coverage report 1`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html coverage report 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", @@ -731,6 +783,15 @@ exports[`application generator > should run successfully with flake8 linter and "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, "lint": { "executor": "@nxlv/python:flake8", "options": { @@ -770,14 +831,14 @@ exports[`application generator > should run successfully with flake8 linter and } `; -exports[`application generator > should run successfully with flake8 linter and pytest with html coverage report 2`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html coverage report 2`] = ` "# test Project description here. " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html coverage report 3`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html coverage report 3`] = ` "[tool.coverage.run] branch = true source = [ "test" ] @@ -800,13 +861,26 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] +[tool.hatch.metadata] +allow-direct-references = true + +[dependency-groups] +dev = [ + "autopep8>=2.3.1", + "flake8>=7.1.1", + "pytest>=8.3.4", + "pytest-sugar>=1.0.0", + "pytest-cov>=6.0.0", + "pytest-html>=4.1.1", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html coverage report 4`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html coverage report 4`] = ` """"Sample Hello World application.""" @@ -816,12 +890,12 @@ def hello(): " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html coverage report 5`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html coverage report 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html coverage report 6`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html coverage report 6`] = ` "[flake8] exclude = .git, @@ -836,7 +910,7 @@ max-line-length = 120 " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html coverage report 7`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html coverage report 7`] = ` """"Hello unit test module.""" from test.hello import hello @@ -848,31 +922,9 @@ def test_hello(): " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html coverage report 8`] = ` -"[project] -name = "nx-workspace" -version = "1.0.0" -dependencies = [ "test" ] - -[dependency-groups] -dev = [ - "flake8>=7.1.1", - "autopep8>=2.3.1", - "pytest>=8.3.4", - "pytest-sugar>=1.0.0", - "pytest-cov>=6.0.0", - "pytest-html>=4.1.1" -] - -[tool.uv.sources.test] -workspace = true - -[tool.uv.workspace] -members = [ "apps/*" ] -" -`; +exports[`application generator > project > should run successfully with flake8 linter and pytest with html coverage report 8`] = `null`; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports 1`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", @@ -902,6 +954,15 @@ exports[`application generator > should run successfully with flake8 linter and "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, "lint": { "executor": "@nxlv/python:flake8", "options": { @@ -941,14 +1002,14 @@ exports[`application generator > should run successfully with flake8 linter and } `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports 2`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports 2`] = ` "# test Project description here. " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports 3`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports 3`] = ` "[tool.coverage.run] branch = true source = [ "test" ] @@ -971,13 +1032,26 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] +[tool.hatch.metadata] +allow-direct-references = true + +[dependency-groups] +dev = [ + "autopep8>=2.3.1", + "flake8>=7.1.1", + "pytest>=8.3.4", + "pytest-sugar>=1.0.0", + "pytest-cov>=6.0.0", + "pytest-html>=4.1.1", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports 4`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports 4`] = ` """"Sample Hello World application.""" @@ -987,12 +1061,12 @@ def hello(): " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports 5`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports 6`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports 6`] = ` "[flake8] exclude = .git, @@ -1007,7 +1081,7 @@ max-line-length = 120 " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports 7`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports 7`] = ` """"Hello unit test module.""" from test.hello import hello @@ -1019,31 +1093,9 @@ def test_hello(): " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports 8`] = ` -"[project] -name = "nx-workspace" -version = "1.0.0" -dependencies = [ "test" ] - -[dependency-groups] -dev = [ - "flake8>=7.1.1", - "autopep8>=2.3.1", - "pytest>=8.3.4", - "pytest-sugar>=1.0.0", - "pytest-cov>=6.0.0", - "pytest-html>=4.1.1" -] - -[tool.uv.sources.test] -workspace = true - -[tool.uv.workspace] -members = [ "apps/*" ] -" -`; +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports 8`] = `null`; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports and threshold 1`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports and threshold 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", @@ -1073,6 +1125,15 @@ exports[`application generator > should run successfully with flake8 linter and "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, "lint": { "executor": "@nxlv/python:flake8", "options": { @@ -1112,14 +1173,14 @@ exports[`application generator > should run successfully with flake8 linter and } `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports and threshold 2`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports and threshold 2`] = ` "# test Project description here. " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports and threshold 3`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports and threshold 3`] = ` "[tool.coverage.run] branch = true source = [ "test" ] @@ -1142,13 +1203,26 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] +[tool.hatch.metadata] +allow-direct-references = true + +[dependency-groups] +dev = [ + "autopep8>=2.3.1", + "flake8>=7.1.1", + "pytest>=8.3.4", + "pytest-sugar>=1.0.0", + "pytest-cov>=6.0.0", + "pytest-html>=4.1.1", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports and threshold 4`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports and threshold 4`] = ` """"Sample Hello World application.""" @@ -1158,12 +1232,12 @@ def hello(): " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports and threshold 5`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports and threshold 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports and threshold 6`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports and threshold 6`] = ` "[flake8] exclude = .git, @@ -1178,7 +1252,7 @@ max-line-length = 120 " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports and threshold 7`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports and threshold 7`] = ` """"Hello unit test module.""" from test.hello import hello @@ -1190,31 +1264,9 @@ def test_hello(): " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports and threshold 8`] = ` -"[project] -name = "nx-workspace" -version = "1.0.0" -dependencies = [ "test" ] - -[dependency-groups] -dev = [ - "flake8>=7.1.1", - "autopep8>=2.3.1", - "pytest>=8.3.4", - "pytest-sugar>=1.0.0", - "pytest-cov>=6.0.0", - "pytest-html>=4.1.1" -] - -[tool.uv.sources.test] -workspace = true - -[tool.uv.workspace] -members = [ "apps/*" ] -" -`; +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports and threshold 8`] = `null`; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit report 1`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit report 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", @@ -1244,6 +1296,15 @@ exports[`application generator > should run successfully with flake8 linter and "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, "lint": { "executor": "@nxlv/python:flake8", "options": { @@ -1283,14 +1344,14 @@ exports[`application generator > should run successfully with flake8 linter and } `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit report 2`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit report 2`] = ` "# test Project description here. " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit report 3`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit report 3`] = ` "[tool.coverage.run] branch = true source = [ "test" ] @@ -1313,13 +1374,26 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] +[tool.hatch.metadata] +allow-direct-references = true + +[dependency-groups] +dev = [ + "autopep8>=2.3.1", + "flake8>=7.1.1", + "pytest>=8.3.4", + "pytest-sugar>=1.0.0", + "pytest-cov>=6.0.0", + "pytest-html>=4.1.1", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit report 4`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit report 4`] = ` """"Sample Hello World application.""" @@ -1329,12 +1403,12 @@ def hello(): " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit report 5`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit report 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit report 6`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit report 6`] = ` "[flake8] exclude = .git, @@ -1349,7 +1423,7 @@ max-line-length = 120 " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit report 7`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit report 7`] = ` """"Hello unit test module.""" from test.hello import hello @@ -1361,31 +1435,9 @@ def test_hello(): " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit report 8`] = ` -"[project] -name = "nx-workspace" -version = "1.0.0" -dependencies = [ "test" ] - -[dependency-groups] -dev = [ - "flake8>=7.1.1", - "autopep8>=2.3.1", - "pytest>=8.3.4", - "pytest-sugar>=1.0.0", - "pytest-cov>=6.0.0", - "pytest-html>=4.1.1" -] - -[tool.uv.sources.test] -workspace = true - -[tool.uv.workspace] -members = [ "apps/*" ] -" -`; +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit report 8`] = `null`; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit,html report 1`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit,html report 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", @@ -1415,6 +1467,15 @@ exports[`application generator > should run successfully with flake8 linter and "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, "lint": { "executor": "@nxlv/python:flake8", "options": { @@ -1454,14 +1515,14 @@ exports[`application generator > should run successfully with flake8 linter and } `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit,html report 2`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit,html report 2`] = ` "# test Project description here. " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit,html report 3`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit,html report 3`] = ` "[tool.coverage.run] branch = true source = [ "test" ] @@ -1484,14 +1545,27 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" -" -`; +[tool.hatch.metadata] +allow-direct-references = true -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit,html report 4`] = ` -""""Sample Hello World application.""" +[dependency-groups] +dev = [ + "autopep8>=2.3.1", + "flake8>=7.1.1", + "pytest>=8.3.4", + "pytest-sugar>=1.0.0", + "pytest-cov>=6.0.0", + "pytest-html>=4.1.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" +" +`; + +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit,html report 4`] = ` +""""Sample Hello World application.""" def hello(): @@ -1500,12 +1574,12 @@ def hello(): " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit,html report 5`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit,html report 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit,html report 6`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit,html report 6`] = ` "[flake8] exclude = .git, @@ -1520,7 +1594,7 @@ max-line-length = 120 " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit,html report 7`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit,html report 7`] = ` """"Hello unit test module.""" from test.hello import hello @@ -1532,31 +1606,9 @@ def test_hello(): " `; -exports[`application generator > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit,html report 8`] = ` -"[project] -name = "nx-workspace" -version = "1.0.0" -dependencies = [ "test" ] +exports[`application generator > project > should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit,html report 8`] = `null`; -[dependency-groups] -dev = [ - "flake8>=7.1.1", - "autopep8>=2.3.1", - "pytest>=8.3.4", - "pytest-sugar>=1.0.0", - "pytest-cov>=6.0.0", - "pytest-html>=4.1.1" -] - -[tool.uv.sources.test] -workspace = true - -[tool.uv.workspace] -members = [ "apps/*" ] -" -`; - -exports[`application generator > should run successfully with flake8 linter and pytest with no reports 1`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with no reports 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", @@ -1586,6 +1638,15 @@ exports[`application generator > should run successfully with flake8 linter and "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, "lint": { "executor": "@nxlv/python:flake8", "options": { @@ -1625,14 +1686,14 @@ exports[`application generator > should run successfully with flake8 linter and } `; -exports[`application generator > should run successfully with flake8 linter and pytest with no reports 2`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with no reports 2`] = ` "# test Project description here. " `; -exports[`application generator > should run successfully with flake8 linter and pytest with no reports 3`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with no reports 3`] = ` "[project] name = "test" version = "1.0.0" @@ -1644,13 +1705,24 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] +[tool.hatch.metadata] +allow-direct-references = true + +[dependency-groups] +dev = [ + "autopep8>=2.3.1", + "flake8>=7.1.1", + "pytest>=8.3.4", + "pytest-sugar>=1.0.0", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" " `; -exports[`application generator > should run successfully with flake8 linter and pytest with no reports 4`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with no reports 4`] = ` """"Sample Hello World application.""" @@ -1660,12 +1732,12 @@ def hello(): " `; -exports[`application generator > should run successfully with flake8 linter and pytest with no reports 5`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with no reports 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with flake8 linter and pytest with no reports 6`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with no reports 6`] = ` "[flake8] exclude = .git, @@ -1680,7 +1752,7 @@ max-line-length = 120 " `; -exports[`application generator > should run successfully with flake8 linter and pytest with no reports 7`] = ` +exports[`application generator > project > should run successfully with flake8 linter and pytest with no reports 7`] = ` """"Hello unit test module.""" from test.hello import hello @@ -1692,29 +1764,9 @@ def test_hello(): " `; -exports[`application generator > should run successfully with flake8 linter and pytest with no reports 8`] = ` -"[project] -name = "nx-workspace" -version = "1.0.0" -dependencies = [ "test" ] - -[dependency-groups] -dev = [ - "flake8>=7.1.1", - "autopep8>=2.3.1", - "pytest>=8.3.4", - "pytest-sugar>=1.0.0" -] - -[tool.uv.sources.test] -workspace = true - -[tool.uv.workspace] -members = [ "apps/*" ] -" -`; +exports[`application generator > project > should run successfully with flake8 linter and pytest with no reports 8`] = `null`; -exports[`application generator > should run successfully with linting (flake8) and testing options with a dev dependency project 1`] = ` +exports[`application generator > project > should run successfully with linting (flake8) and testing options with a dev dependency project 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", @@ -1744,6 +1796,15 @@ exports[`application generator > should run successfully with linting (flake8) a "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, "lint": { "executor": "@nxlv/python:flake8", "options": { @@ -1783,14 +1844,14 @@ exports[`application generator > should run successfully with linting (flake8) a } `; -exports[`application generator > should run successfully with linting (flake8) and testing options with a dev dependency project 2`] = ` +exports[`application generator > project > should run successfully with linting (flake8) and testing options with a dev dependency project 2`] = ` "# test Project description here. " `; -exports[`application generator > should run successfully with linting (flake8) and testing options with a dev dependency project 3`] = ` +exports[`application generator > project > should run successfully with linting (flake8) and testing options with a dev dependency project 3`] = ` "[tool.coverage.run] branch = true source = [ "test" ] @@ -1813,13 +1874,16 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] +[tool.hatch.metadata] +allow-direct-references = true + [dependency-groups] dev = [ - "shared-dev-lib" + "shared-dev-lib", ] [tool.uv.sources] -shared-dev-lib = { workspace = true } +shared-dev-lib = { path = "../../libs/shared/dev-lib" } [build-system] requires = ["hatchling"] @@ -1827,7 +1891,7 @@ build-backend = "hatchling.build" " `; -exports[`application generator > should run successfully with linting (flake8) and testing options with a dev dependency project 4`] = ` +exports[`application generator > project > should run successfully with linting (flake8) and testing options with a dev dependency project 4`] = ` """"Sample Hello World application.""" @@ -1837,12 +1901,12 @@ def hello(): " `; -exports[`application generator > should run successfully with linting (flake8) and testing options with a dev dependency project 5`] = ` +exports[`application generator > project > should run successfully with linting (flake8) and testing options with a dev dependency project 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with linting (flake8) and testing options with a dev dependency project 6`] = ` +exports[`application generator > project > should run successfully with linting (flake8) and testing options with a dev dependency project 6`] = ` "[flake8] exclude = .git, @@ -1857,7 +1921,7 @@ max-line-length = 120 " `; -exports[`application generator > should run successfully with linting (flake8) and testing options with a dev dependency project 7`] = ` +exports[`application generator > project > should run successfully with linting (flake8) and testing options with a dev dependency project 7`] = ` """"Hello unit test module.""" from test.hello import hello @@ -1869,14 +1933,14 @@ def test_hello(): " `; -exports[`application generator > should run successfully with linting (flake8) and testing options with a dev dependency project 8`] = ` +exports[`application generator > project > should run successfully with linting (flake8) and testing options with a dev dependency project 8`] = ` "# shared-dev-lib Project description here. " `; -exports[`application generator > should run successfully with linting (flake8) and testing options with a dev dependency project 9`] = ` +exports[`application generator > project > should run successfully with linting (flake8) and testing options with a dev dependency project 9`] = ` "[project] name = "shared-dev-lib" version = "1.0.0" @@ -1895,13 +1959,19 @@ dependencies = [ [tool.hatch.build.targets.wheel] packages = [ "shared_dev_lib" ] +[tool.hatch.metadata] +allow-direct-references = true + +[dependency-groups] +dev = [ "autopep8>=2.3.1" ] + [build-system] requires = [ "hatchling" ] build-backend = "hatchling.build" " `; -exports[`application generator > should run successfully with linting (flake8) and testing options with a dev dependency project 10`] = ` +exports[`application generator > project > should run successfully with linting (flake8) and testing options with a dev dependency project 10`] = ` """"Sample Hello World application.""" @@ -1911,32 +1981,14 @@ def hello(): " `; -exports[`application generator > should run successfully with linting (flake8) and testing options with a dev dependency project 11`] = ` +exports[`application generator > project > should run successfully with linting (flake8) and testing options with a dev dependency project 11`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with linting (flake8) and testing options with a dev dependency project 12`] = ` -"[project] -name = "nx-workspace" -version = "1.0.0" -dependencies = [ "shared-dev-lib", "test" ] - -[dependency-groups] -dev = [ "autopep8>=2.3.1" ] - -[tool.uv.sources.shared-dev-lib] -workspace = true +exports[`application generator > project > should run successfully with linting (flake8) and testing options with a dev dependency project 12`] = `null`; -[tool.uv.sources.test] -workspace = true - -[tool.uv.workspace] -members = [ "libs/*", "apps/*" ] -" -`; - -exports[`application generator > should run successfully with linting (ruff) and testing options with a dev dependency project 1`] = ` +exports[`application generator > project > should run successfully with linting (ruff) and testing options with a dev dependency project 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", @@ -1966,6 +2018,15 @@ exports[`application generator > should run successfully with linting (ruff) and "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, "lint": { "executor": "@nxlv/python:ruff-check", "options": { @@ -2006,14 +2067,14 @@ exports[`application generator > should run successfully with linting (ruff) and } `; -exports[`application generator > should run successfully with linting (ruff) and testing options with a dev dependency project 2`] = ` +exports[`application generator > project > should run successfully with linting (ruff) and testing options with a dev dependency project 2`] = ` "# test Project description here. " `; -exports[`application generator > should run successfully with linting (ruff) and testing options with a dev dependency project 3`] = ` +exports[`application generator > project > should run successfully with linting (ruff) and testing options with a dev dependency project 3`] = ` "[tool.coverage.run] branch = true source = [ "test" ] @@ -2036,13 +2097,16 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] +[tool.hatch.metadata] +allow-direct-references = true + [dependency-groups] dev = [ - "shared-dev-lib" + "shared-dev-lib", ] [tool.uv.sources] -shared-dev-lib = { workspace = true } +shared-dev-lib = { path = "../../libs/shared/dev-lib" } [build-system] requires = ["hatchling"] @@ -2082,7 +2146,7 @@ unfixable = [] " `; -exports[`application generator > should run successfully with linting (ruff) and testing options with a dev dependency project 4`] = ` +exports[`application generator > project > should run successfully with linting (ruff) and testing options with a dev dependency project 4`] = ` """"Sample Hello World application.""" @@ -2092,12 +2156,12 @@ def hello(): " `; -exports[`application generator > should run successfully with linting (ruff) and testing options with a dev dependency project 5`] = ` +exports[`application generator > project > should run successfully with linting (ruff) and testing options with a dev dependency project 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with linting (ruff) and testing options with a dev dependency project 6`] = ` +exports[`application generator > project > should run successfully with linting (ruff) and testing options with a dev dependency project 6`] = ` """"Hello unit test module.""" from test.hello import hello @@ -2109,14 +2173,14 @@ def test_hello(): " `; -exports[`application generator > should run successfully with linting (ruff) and testing options with a dev dependency project 7`] = ` +exports[`application generator > project > should run successfully with linting (ruff) and testing options with a dev dependency project 7`] = ` "# shared-dev-lib Project description here. " `; -exports[`application generator > should run successfully with linting (ruff) and testing options with a dev dependency project 8`] = ` +exports[`application generator > project > should run successfully with linting (ruff) and testing options with a dev dependency project 8`] = ` "[project] name = "shared-dev-lib" version = "1.0.0" @@ -2135,13 +2199,19 @@ dependencies = [ [tool.hatch.build.targets.wheel] packages = [ "shared_dev_lib" ] +[tool.hatch.metadata] +allow-direct-references = true + +[dependency-groups] +dev = [ "autopep8>=2.3.1" ] + [build-system] requires = [ "hatchling" ] build-backend = "hatchling.build" " `; -exports[`application generator > should run successfully with linting (ruff) and testing options with a dev dependency project 9`] = ` +exports[`application generator > project > should run successfully with linting (ruff) and testing options with a dev dependency project 9`] = ` """"Sample Hello World application.""" @@ -2151,32 +2221,14 @@ def hello(): " `; -exports[`application generator > should run successfully with linting (ruff) and testing options with a dev dependency project 10`] = ` +exports[`application generator > project > should run successfully with linting (ruff) and testing options with a dev dependency project 10`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with linting (ruff) and testing options with a dev dependency project 11`] = ` -"[project] -name = "nx-workspace" -version = "1.0.0" -dependencies = [ "shared-dev-lib", "test" ] - -[dependency-groups] -dev = [ "autopep8>=2.3.1" ] +exports[`application generator > project > should run successfully with linting (ruff) and testing options with a dev dependency project 11`] = `null`; -[tool.uv.sources.shared-dev-lib] -workspace = true - -[tool.uv.sources.test] -workspace = true - -[tool.uv.workspace] -members = [ "libs/*", "apps/*" ] -" -`; - -exports[`application generator > should run successfully with linting and testing options with a dev dependency project with custom package name 1`] = ` +exports[`application generator > project > should run successfully with linting and testing options with a dev dependency project with custom package name 1`] = ` "[tool.coverage.run] branch = true source = [ "test" ] @@ -2199,13 +2251,16 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] +[tool.hatch.metadata] +allow-direct-references = true + [dependency-groups] dev = [ - "custom-shared-dev-lib" + "custom-shared-dev-lib", ] [tool.uv.sources] -custom-shared-dev-lib = { workspace = true } +custom-shared-dev-lib = { path = "../../libs/shared/dev-lib" } [build-system] requires = ["hatchling"] @@ -2213,27 +2268,9 @@ build-backend = "hatchling.build" " `; -exports[`application generator > should run successfully with linting and testing options with a dev dependency project with custom package name 2`] = ` -"[project] -name = "nx-workspace" -version = "1.0.0" -dependencies = [ "custom-shared-dev-lib", "test" ] - -[dependency-groups] -dev = [ "autopep8>=2.3.1" ] - -[tool.uv.sources.custom-shared-dev-lib] -workspace = true - -[tool.uv.sources.test] -workspace = true - -[tool.uv.workspace] -members = [ "libs/*", "apps/*" ] -" -`; +exports[`application generator > project > should run successfully with linting and testing options with a dev dependency project with custom package name 2`] = `null`; -exports[`application generator > should run successfully with linting and testing options with an existing dev dependency project 1`] = ` +exports[`application generator > project > should run successfully with linting and testing options with an existing dev dependency project 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", @@ -2263,6 +2300,15 @@ exports[`application generator > should run successfully with linting and testin "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, "lint": { "executor": "@nxlv/python:flake8", "options": { @@ -2302,14 +2348,14 @@ exports[`application generator > should run successfully with linting and testin } `; -exports[`application generator > should run successfully with linting and testing options with an existing dev dependency project 2`] = ` +exports[`application generator > project > should run successfully with linting and testing options with an existing dev dependency project 2`] = ` "# test Project description here. " `; -exports[`application generator > should run successfully with linting and testing options with an existing dev dependency project 3`] = ` +exports[`application generator > project > should run successfully with linting and testing options with an existing dev dependency project 3`] = ` "[tool.coverage.run] branch = true source = [ "test" ] @@ -2332,13 +2378,16 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] +[tool.hatch.metadata] +allow-direct-references = true + [dependency-groups] dev = [ - "shared-dev-lib" + "shared-dev-lib", ] [tool.uv.sources] -shared-dev-lib = { workspace = true } +shared-dev-lib = { path = "../../libs/shared/dev-lib" } [build-system] requires = ["hatchling"] @@ -2346,7 +2395,7 @@ build-backend = "hatchling.build" " `; -exports[`application generator > should run successfully with linting and testing options with an existing dev dependency project 4`] = ` +exports[`application generator > project > should run successfully with linting and testing options with an existing dev dependency project 4`] = ` """"Sample Hello World application.""" @@ -2356,12 +2405,12 @@ def hello(): " `; -exports[`application generator > should run successfully with linting and testing options with an existing dev dependency project 5`] = ` +exports[`application generator > project > should run successfully with linting and testing options with an existing dev dependency project 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with linting and testing options with an existing dev dependency project 6`] = ` +exports[`application generator > project > should run successfully with linting and testing options with an existing dev dependency project 6`] = ` "[flake8] exclude = .git, @@ -2376,7 +2425,7 @@ max-line-length = 120 " `; -exports[`application generator > should run successfully with linting and testing options with an existing dev dependency project 7`] = ` +exports[`application generator > project > should run successfully with linting and testing options with an existing dev dependency project 7`] = ` """"Hello unit test module.""" from test.hello import hello @@ -2388,14 +2437,14 @@ def test_hello(): " `; -exports[`application generator > should run successfully with linting and testing options with an existing dev dependency project 8`] = ` +exports[`application generator > project > should run successfully with linting and testing options with an existing dev dependency project 8`] = ` "# shared-dev-lib Project description here. " `; -exports[`application generator > should run successfully with linting and testing options with an existing dev dependency project 9`] = ` +exports[`application generator > project > should run successfully with linting and testing options with an existing dev dependency project 9`] = ` "[project] name = "shared-dev-lib" version = "1.0.0" @@ -2415,13 +2464,19 @@ dependencies = [ [tool.hatch.build.targets.wheel] packages = [ "shared_dev_lib" ] +[tool.hatch.metadata] +allow-direct-references = true + +[dependency-groups] +dev = [ "autopep8>=2.3.1" ] + [build-system] requires = [ "hatchling" ] build-backend = "hatchling.build" " `; -exports[`application generator > should run successfully with linting and testing options with an existing dev dependency project 10`] = ` +exports[`application generator > project > should run successfully with linting and testing options with an existing dev dependency project 10`] = ` """"Sample Hello World application.""" @@ -2431,12 +2486,12 @@ def hello(): " `; -exports[`application generator > should run successfully with linting and testing options with an existing dev dependency project 11`] = ` +exports[`application generator > project > should run successfully with linting and testing options with an existing dev dependency project 11`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with minimal options 1`] = ` +exports[`application generator > project > should run successfully with ruff linter 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", @@ -2466,6 +2521,24 @@ exports[`application generator > should run successfully with minimal options 1` "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, + "lint": { + "executor": "@nxlv/python:ruff-check", + "options": { + "lintFilePatterns": [ + "test", + ], + }, + "outputs": [], + }, "lock": { "executor": "@nxlv/python:run-commands", "options": { @@ -2485,14 +2558,14 @@ exports[`application generator > should run successfully with minimal options 1` } `; -exports[`application generator > should run successfully with minimal options 2`] = ` +exports[`application generator > project > should run successfully with ruff linter 2`] = ` "# test Project description here. " `; -exports[`application generator > should run successfully with minimal options 3`] = ` +exports[`application generator > project > should run successfully with ruff linter 3`] = ` "[project] name = "test" version = "1.0.0" @@ -2504,13 +2577,54 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] +[tool.hatch.metadata] +allow-direct-references = true + +[dependency-groups] +dev = [ + "autopep8>=2.3.1", + "ruff>=0.8.2", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" + +[tool.ruff] +exclude = [ + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "dist", +] + +line-length = 88 +indent-width = 4 + +[tool.ruff.lint] +select = [ + # pycodestyle + "E", + # Pyflakes + "F", + # pyupgrade + "UP", + # flake8-bugbear + "B", + # flake8-simplify + "SIM", + # isort + "I", +] +ignore = [] + +fixable = ["ALL"] +unfixable = [] " `; -exports[`application generator > should run successfully with minimal options 4`] = ` +exports[`application generator > project > should run successfully with ruff linter 4`] = ` """"Sample Hello World application.""" @@ -2520,29 +2634,14 @@ def hello(): " `; -exports[`application generator > should run successfully with minimal options 5`] = ` +exports[`application generator > project > should run successfully with ruff linter 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with minimal options 6`] = ` -"[project] -name = "nx-workspace" -version = "1.0.0" -dependencies = [ "test" ] - -[dependency-groups] -dev = [ "autopep8>=2.3.1" ] - -[tool.uv.sources.test] -workspace = true +exports[`application generator > project > should run successfully with ruff linter 6`] = `null`; -[tool.uv.workspace] -members = [ "apps/*" ] -" -`; - -exports[`application generator > should run successfully with minimal options with custom rootPyprojectDependencyGroup 1`] = ` +exports[`application generator > project > should run successfully with ruff linter and pytest with no reports 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", @@ -2572,6 +2671,25 @@ exports[`application generator > should run successfully with minimal options wi "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, + "lint": { + "executor": "@nxlv/python:ruff-check", + "options": { + "lintFilePatterns": [ + "test", + "tests", + ], + }, + "outputs": [], + }, "lock": { "executor": "@nxlv/python:run-commands", "options": { @@ -2583,6 +2701,17 @@ exports[`application generator > should run successfully with minimal options wi "executor": "@nxlv/python:remove", "options": {}, }, + "test": { + "executor": "@nxlv/python:run-commands", + "options": { + "command": "uv run pytest tests/", + "cwd": "apps/test", + }, + "outputs": [ + "{workspaceRoot}/reports/apps/test/unittests", + "{workspaceRoot}/coverage/apps/test", + ], + }, "update": { "executor": "@nxlv/python:update", "options": {}, @@ -2591,14 +2720,14 @@ exports[`application generator > should run successfully with minimal options wi } `; -exports[`application generator > should run successfully with minimal options with custom rootPyprojectDependencyGroup 2`] = ` +exports[`application generator > project > should run successfully with ruff linter and pytest with no reports 2`] = ` "# test Project description here. " `; -exports[`application generator > should run successfully with minimal options with custom rootPyprojectDependencyGroup 3`] = ` +exports[`application generator > project > should run successfully with ruff linter and pytest with no reports 3`] = ` "[project] name = "test" version = "1.0.0" @@ -2610,13 +2739,56 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] +[tool.hatch.metadata] +allow-direct-references = true + +[dependency-groups] +dev = [ + "autopep8>=2.3.1", + "ruff>=0.8.2", + "pytest>=8.3.4", + "pytest-sugar>=1.0.0", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" + +[tool.ruff] +exclude = [ + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "dist", +] + +line-length = 88 +indent-width = 4 + +[tool.ruff.lint] +select = [ + # pycodestyle + "E", + # Pyflakes + "F", + # pyupgrade + "UP", + # flake8-bugbear + "B", + # flake8-simplify + "SIM", + # isort + "I", +] +ignore = [] + +fixable = ["ALL"] +unfixable = [] " `; -exports[`application generator > should run successfully with minimal options with custom rootPyprojectDependencyGroup 4`] = ` +exports[`application generator > project > should run successfully with ruff linter and pytest with no reports 4`] = ` """"Sample Hello World application.""" @@ -2626,29 +2798,26 @@ def hello(): " `; -exports[`application generator > should run successfully with minimal options with custom rootPyprojectDependencyGroup 5`] = ` +exports[`application generator > project > should run successfully with ruff linter and pytest with no reports 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with minimal options with custom rootPyprojectDependencyGroup 6`] = ` -"[project] -name = "nx-workspace" -version = "1.0.0" -dependencies = [ ] +exports[`application generator > project > should run successfully with ruff linter and pytest with no reports 6`] = ` +""""Hello unit test module.""" -[dependency-groups] -dev = [ "test", "autopep8>=2.3.1" ] +from test.hello import hello -[tool.uv.sources.test] -workspace = true -[tool.uv.workspace] -members = [ "apps/*" ] +def test_hello(): + """Test the hello function.""" + assert hello() == "Hello test" " `; -exports[`application generator > should run successfully with minimal options with existing custom rootPyprojectDependencyGroup 1`] = ` +exports[`application generator > project > should run successfully with ruff linter and pytest with no reports 7`] = `null`; + +exports[`application generator > should run successfully with minimal options 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", @@ -2678,6 +2847,15 @@ exports[`application generator > should run successfully with minimal options wi "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, "lock": { "executor": "@nxlv/python:run-commands", "options": { @@ -2697,14 +2875,14 @@ exports[`application generator > should run successfully with minimal options wi } `; -exports[`application generator > should run successfully with minimal options with existing custom rootPyprojectDependencyGroup 2`] = ` +exports[`application generator > should run successfully with minimal options 2`] = ` "# test Project description here. " `; -exports[`application generator > should run successfully with minimal options with existing custom rootPyprojectDependencyGroup 3`] = ` +exports[`application generator > should run successfully with minimal options 3`] = ` "[project] name = "test" version = "1.0.0" @@ -2716,13 +2894,21 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] +[tool.hatch.metadata] +allow-direct-references = true + +[dependency-groups] +dev = [ + "autopep8>=2.3.1", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" " `; -exports[`application generator > should run successfully with minimal options with existing custom rootPyprojectDependencyGroup 4`] = ` +exports[`application generator > should run successfully with minimal options 4`] = ` """"Sample Hello World application.""" @@ -2732,29 +2918,14 @@ def hello(): " `; -exports[`application generator > should run successfully with minimal options with existing custom rootPyprojectDependencyGroup 5`] = ` +exports[`application generator > should run successfully with minimal options 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with minimal options with existing custom rootPyprojectDependencyGroup 6`] = ` -"[project] -name = "nx-workspace" -version = "1.0.0" -dependencies = [ ] - -[dependency-groups] -dev = [ "requests>=2.3.1", "test", "autopep8>=2.3.1" ] - -[tool.uv.sources.test] -workspace = true - -[tool.uv.workspace] -members = [ "apps/*" ] -" -`; +exports[`application generator > should run successfully with minimal options 6`] = `null`; -exports[`application generator > should run successfully with minimal options without rootPyprojectDependencyGroup 1`] = ` +exports[`application generator > workspace > should run successfully with minimal options with custom rootPyprojectDependencyGroup 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", @@ -2784,6 +2955,15 @@ exports[`application generator > should run successfully with minimal options wi "{projectRoot}/dist", ], }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "args": "", + "debug": false, + "silent": false, + "verbose": false, + }, + }, "lock": { "executor": "@nxlv/python:run-commands", "options": { @@ -2803,14 +2983,14 @@ exports[`application generator > should run successfully with minimal options wi } `; -exports[`application generator > should run successfully with minimal options without rootPyprojectDependencyGroup 2`] = ` +exports[`application generator > workspace > should run successfully with minimal options with custom rootPyprojectDependencyGroup 2`] = ` "# test Project description here. " `; -exports[`application generator > should run successfully with minimal options without rootPyprojectDependencyGroup 3`] = ` +exports[`application generator > workspace > should run successfully with minimal options with custom rootPyprojectDependencyGroup 3`] = ` "[project] name = "test" version = "1.0.0" @@ -2822,13 +3002,16 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] +[tool.hatch.metadata] +allow-direct-references = true + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" " `; -exports[`application generator > should run successfully with minimal options without rootPyprojectDependencyGroup 4`] = ` +exports[`application generator > workspace > should run successfully with minimal options with custom rootPyprojectDependencyGroup 4`] = ` """"Sample Hello World application.""" @@ -2838,29 +3021,29 @@ def hello(): " `; -exports[`application generator > should run successfully with minimal options without rootPyprojectDependencyGroup 5`] = ` +exports[`application generator > workspace > should run successfully with minimal options with custom rootPyprojectDependencyGroup 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with minimal options without rootPyprojectDependencyGroup 6`] = ` +exports[`application generator > workspace > should run successfully with minimal options with custom rootPyprojectDependencyGroup 6`] = ` "[project] name = "nx-workspace" version = "1.0.0" -dependencies = [ "test" ] +dependencies = [ ] [dependency-groups] -dev = [ "autopep8>=2.3.1" ] +dev = [ "test", "autopep8>=2.3.1" ] [tool.uv.sources.test] workspace = true [tool.uv.workspace] -members = [ "apps/*" ] +members = [ "apps/test" ] " `; -exports[`application generator > should run successfully with ruff linter 1`] = ` +exports[`application generator > workspace > should run successfully with minimal options with existing custom rootPyprojectDependencyGroup 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", @@ -2890,14 +3073,14 @@ exports[`application generator > should run successfully with ruff linter 1`] = "{projectRoot}/dist", ], }, - "lint": { - "executor": "@nxlv/python:ruff-check", + "install": { + "executor": "@nxlv/python:install", "options": { - "lintFilePatterns": [ - "test", - ], + "args": "", + "debug": false, + "silent": false, + "verbose": false, }, - "outputs": [], }, "lock": { "executor": "@nxlv/python:run-commands", @@ -2918,14 +3101,14 @@ exports[`application generator > should run successfully with ruff linter 1`] = } `; -exports[`application generator > should run successfully with ruff linter 2`] = ` +exports[`application generator > workspace > should run successfully with minimal options with existing custom rootPyprojectDependencyGroup 2`] = ` "# test Project description here. " `; -exports[`application generator > should run successfully with ruff linter 3`] = ` +exports[`application generator > workspace > should run successfully with minimal options with existing custom rootPyprojectDependencyGroup 3`] = ` "[project] name = "test" version = "1.0.0" @@ -2937,45 +3120,16 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] +[tool.hatch.metadata] +allow-direct-references = true + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" - -[tool.ruff] -exclude = [ - ".ruff_cache", - ".svn", - ".tox", - ".venv", - "dist", -] - -line-length = 88 -indent-width = 4 - -[tool.ruff.lint] -select = [ - # pycodestyle - "E", - # Pyflakes - "F", - # pyupgrade - "UP", - # flake8-bugbear - "B", - # flake8-simplify - "SIM", - # isort - "I", -] -ignore = [] - -fixable = ["ALL"] -unfixable = [] " `; -exports[`application generator > should run successfully with ruff linter 4`] = ` +exports[`application generator > workspace > should run successfully with minimal options with existing custom rootPyprojectDependencyGroup 4`] = ` """"Sample Hello World application.""" @@ -2985,29 +3139,29 @@ def hello(): " `; -exports[`application generator > should run successfully with ruff linter 5`] = ` +exports[`application generator > workspace > should run successfully with minimal options with existing custom rootPyprojectDependencyGroup 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with ruff linter 6`] = ` +exports[`application generator > workspace > should run successfully with minimal options with existing custom rootPyprojectDependencyGroup 6`] = ` "[project] name = "nx-workspace" version = "1.0.0" -dependencies = [ "test" ] +dependencies = [ ] [dependency-groups] -dev = [ "ruff>=0.8.2", "autopep8>=2.3.1" ] +dev = [ "requests>=2.3.1", "test", "autopep8>=2.3.1" ] [tool.uv.sources.test] workspace = true [tool.uv.workspace] -members = [ "apps/*" ] +members = [ "apps/test" ] " `; -exports[`application generator > should run successfully with ruff linter and pytest with no reports 1`] = ` +exports[`application generator > workspace > should run successfully with minimal options without rootPyprojectDependencyGroup 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", @@ -3037,15 +3191,14 @@ exports[`application generator > should run successfully with ruff linter and py "{projectRoot}/dist", ], }, - "lint": { - "executor": "@nxlv/python:ruff-check", + "install": { + "executor": "@nxlv/python:install", "options": { - "lintFilePatterns": [ - "test", - "tests", - ], + "args": "", + "debug": false, + "silent": false, + "verbose": false, }, - "outputs": [], }, "lock": { "executor": "@nxlv/python:run-commands", @@ -3058,17 +3211,6 @@ exports[`application generator > should run successfully with ruff linter and py "executor": "@nxlv/python:remove", "options": {}, }, - "test": { - "executor": "@nxlv/python:run-commands", - "options": { - "command": "uv run pytest tests/", - "cwd": "apps/test", - }, - "outputs": [ - "{workspaceRoot}/reports/apps/test/unittests", - "{workspaceRoot}/coverage/apps/test", - ], - }, "update": { "executor": "@nxlv/python:update", "options": {}, @@ -3077,14 +3219,14 @@ exports[`application generator > should run successfully with ruff linter and py } `; -exports[`application generator > should run successfully with ruff linter and pytest with no reports 2`] = ` +exports[`application generator > workspace > should run successfully with minimal options without rootPyprojectDependencyGroup 2`] = ` "# test Project description here. " `; -exports[`application generator > should run successfully with ruff linter and pytest with no reports 3`] = ` +exports[`application generator > workspace > should run successfully with minimal options without rootPyprojectDependencyGroup 3`] = ` "[project] name = "test" version = "1.0.0" @@ -3096,45 +3238,16 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["test"] +[tool.hatch.metadata] +allow-direct-references = true + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" - -[tool.ruff] -exclude = [ - ".ruff_cache", - ".svn", - ".tox", - ".venv", - "dist", -] - -line-length = 88 -indent-width = 4 - -[tool.ruff.lint] -select = [ - # pycodestyle - "E", - # Pyflakes - "F", - # pyupgrade - "UP", - # flake8-bugbear - "B", - # flake8-simplify - "SIM", - # isort - "I", -] -ignore = [] - -fixable = ["ALL"] -unfixable = [] " `; -exports[`application generator > should run successfully with ruff linter and pytest with no reports 4`] = ` +exports[`application generator > workspace > should run successfully with minimal options without rootPyprojectDependencyGroup 4`] = ` """"Sample Hello World application.""" @@ -3144,41 +3257,24 @@ def hello(): " `; -exports[`application generator > should run successfully with ruff linter and pytest with no reports 5`] = ` +exports[`application generator > workspace > should run successfully with minimal options without rootPyprojectDependencyGroup 5`] = ` "3.9.7 " `; -exports[`application generator > should run successfully with ruff linter and pytest with no reports 6`] = ` -""""Hello unit test module.""" - -from test.hello import hello - - -def test_hello(): - """Test the hello function.""" - assert hello() == "Hello test" -" -`; - -exports[`application generator > should run successfully with ruff linter and pytest with no reports 7`] = ` +exports[`application generator > workspace > should run successfully with minimal options without rootPyprojectDependencyGroup 6`] = ` "[project] name = "nx-workspace" version = "1.0.0" dependencies = [ "test" ] -[dependency-groups] -dev = [ - "ruff>=0.8.2", - "autopep8>=2.3.1", - "pytest>=8.3.4", - "pytest-sugar>=1.0.0" -] - [tool.uv.sources.test] workspace = true [tool.uv.workspace] -members = [ "apps/*" ] +members = [ "apps/test" ] + +[dependency-groups] +dev = [ "autopep8>=2.3.1" ] " `; diff --git a/packages/nx-python/src/generators/uv-project/files/base/pyproject.toml b/packages/nx-python/src/generators/uv-project/files/base/pyproject.toml index bee72bd..a4e6ec2 100644 --- a/packages/nx-python/src/generators/uv-project/files/base/pyproject.toml +++ b/packages/nx-python/src/generators/uv-project/files/base/pyproject.toml @@ -24,16 +24,47 @@ dependencies = [] [tool.hatch.build.targets.wheel] packages = ["<%= moduleName %>"] -<%if (devDependenciesProject !== '') { -%> +[tool.hatch.metadata] +allow-direct-references = true +<%if (devDependenciesProject !== '' || individualPackage) { -%> + [dependency-groups] dev = [ - "<%- devDependenciesProjectPkgName %>" +<%if (devDependenciesProject !== '') { -%> + "<%- devDependenciesProjectPkgName %>", +<% } -%> +<%if ((individualPackage && !devDependenciesProject) || (individualPackage && !devDependenciesProject)) { -%> + "autopep8>=<%- versionMap['autopep8'] %>", +<%if (individualPackage && !devDependenciesProject && linter === 'flake8') { -%> + "flake8>=<%- versionMap['flake8'] %>", +<% } -%> +<%if (individualPackage && !devDependenciesProject && linter === 'ruff') { -%> + "ruff>=<%- versionMap['ruff'] %>", +<% } -%> +<%if (individualPackage && !devDependenciesProject && unitTestRunner === 'pytest') { -%> + "pytest>=<%- versionMap['pytest'] %>", + "pytest-sugar>=<%- versionMap['pytest-sugar'] %>", +<% } -%> +<%if (individualPackage && !devDependenciesProject && unitTestRunner === 'pytest' && codeCoverage) { -%> + "pytest-cov>=<%- versionMap['pytest-cov'] %>", +<% } -%> +<%if (individualPackage && !devDependenciesProject && unitTestRunner === 'pytest' && codeCoverage && codeCoverageHtmlReport) { -%> + "pytest-html>=<%- versionMap['pytest-html'] %>", +<% } -%> +<% } -%> ] +<% } -%> +<%if (devDependenciesProject !== '') { -%> [tool.uv.sources] +<%if (individualPackage) { -%> +<%- devDependenciesProjectPkgName %> = { path = "<%- devDependenciesProjectPath %>" } +<% } -%> +<%if (!individualPackage) { -%> <%- devDependenciesProjectPkgName %> = { workspace = true } - <% } -%> +<% } -%> + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/packages/nx-python/src/generators/uv-project/generator.spec.ts b/packages/nx-python/src/generators/uv-project/generator.spec.ts index d660a26..9bcf93d 100644 --- a/packages/nx-python/src/generators/uv-project/generator.spec.ts +++ b/packages/nx-python/src/generators/uv-project/generator.spec.ts @@ -62,10 +62,10 @@ describe('application generator', () => { }); }); - it('should throw an exception when the poetry is not installed', async () => { - checkUvExecutable.mockRejectedValue(new Error('poetry not found')); + it('should throw an exception when the uv is not installed', async () => { + checkUvExecutable.mockRejectedValue(new Error('uv not found')); - expect(generator(appTree, options)).rejects.toThrow('poetry not found'); + expect(generator(appTree, options)).rejects.toThrow('uv not found'); expect(checkUvExecutable).toHaveBeenCalled(); }); @@ -129,463 +129,497 @@ describe('application generator', () => { expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - expect(spawn.sync).toHaveBeenCalledTimes(2); + expect(spawn.sync).toHaveBeenCalledTimes(1); expect(spawn.sync).toHaveBeenNthCalledWith(1, 'python', ['--version'], { stdio: 'pipe', }); - expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['sync'], { - shell: false, - stdio: 'inherit', - }); }); - it('should run successfully with minimal options without rootPyprojectDependencyGroup', async () => { - const callbackTask = await generator(appTree, { - ...options, - rootPyprojectDependencyGroup: undefined, - }); - callbackTask(); - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toMatchSnapshot(); + describe('project', () => { + it('should run successfully minimal configuration as a library', async () => { + await generator(appTree, { + ...options, + projectType: 'library', + }); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toMatchSnapshot(); - const projectDirectory = 'apps/test'; - const moduleName = 'test'; + const projectDirectory = 'libs/test'; + const moduleName = 'test'; - assertGeneratedFilesBase(appTree, projectDirectory, moduleName); + assertGeneratedFilesBase(appTree, projectDirectory, moduleName); - expect(appTree.exists(`${projectDirectory}/.flake8`)).toBeFalsy(); - expect( - appTree.exists(`${projectDirectory}/tests/test_hello.py`), - ).toBeFalsy(); + expect(appTree.exists(`${projectDirectory}/.flake8`)).toBeFalsy(); + expect( + appTree.exists(`${projectDirectory}/tests/test_hello.py`), + ).toBeFalsy(); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); + }); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); + it('should run successfully minimal configuration with tags', async () => { + await generator(appTree, { + ...options, + tags: 'one,two', + }); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toMatchSnapshot(); - expect(spawn.sync).toHaveBeenCalledTimes(2); - expect(spawn.sync).toHaveBeenNthCalledWith(1, 'python', ['--version'], { - stdio: 'pipe', - }); - expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['sync'], { - shell: false, - stdio: 'inherit', - }); - }); + const projectDirectory = 'apps/test'; + const moduleName = 'test'; - it('should run successfully with minimal options with custom rootPyprojectDependencyGroup', async () => { - const callbackTask = await generator(appTree, { - ...options, - rootPyprojectDependencyGroup: 'dev', + assertGeneratedFilesBase(appTree, projectDirectory, moduleName); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); }); - callbackTask(); - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toMatchSnapshot(); - const projectDirectory = 'apps/test'; - const moduleName = 'test'; + it('should run successfully minimal configuration custom directory', async () => { + await generator(appTree, { + ...options, + directory: 'subdir', + }); + const config = readProjectConfiguration(appTree, 'subdir-test'); + expect(config).toMatchSnapshot(); - assertGeneratedFilesBase(appTree, projectDirectory, moduleName); + const projectDirectory = 'apps/subdir/test'; + const moduleName = 'subdir_test'; - expect(appTree.exists(`${projectDirectory}/.flake8`)).toBeFalsy(); - expect( - appTree.exists(`${projectDirectory}/tests/test_hello.py`), - ).toBeFalsy(); + assertGeneratedFilesBase(appTree, projectDirectory, moduleName); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); + }); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); + it('should run successfully with flake8 linter', async () => { + await generator(appTree, { + ...options, + linter: 'flake8', + }); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toMatchSnapshot(); - expect(spawn.sync).toHaveBeenCalledTimes(2); - expect(spawn.sync).toHaveBeenNthCalledWith(1, 'python', ['--version'], { - stdio: 'pipe', - }); - expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['sync'], { - shell: false, - stdio: 'inherit', + assertGeneratedFilesBase(appTree, 'apps/test', 'test'); + assertGeneratedFilesFlake8(appTree, 'apps/test'); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); }); - }); - it('should run successfully with minimal options with existing custom rootPyprojectDependencyGroup', async () => { - appTree.write( - 'pyproject.toml', - dedent` - [project] - name = "nx-workspace" - version = "1.0.0" - dependencies = [ ] - - [dependency-groups] - dev = [ "requests>=2.3.1" ] - `, - ); - - const callbackTask = await generator(appTree, { - ...options, - rootPyprojectDependencyGroup: 'dev', - }); - callbackTask(); - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toMatchSnapshot(); + it('should run successfully with ruff linter', async () => { + await generator(appTree, { + ...options, + linter: 'ruff', + }); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toMatchSnapshot(); - const projectDirectory = 'apps/test'; - const moduleName = 'test'; + assertGeneratedFilesBase(appTree, 'apps/test', 'test'); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); + }); - assertGeneratedFilesBase(appTree, projectDirectory, moduleName); + it('should run successfully with flake8 linter and pytest with no reports', async () => { + await generator(appTree, { + ...options, + linter: 'flake8', + unitTestRunner: 'pytest', + }); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toMatchSnapshot(); - expect(appTree.exists(`${projectDirectory}/.flake8`)).toBeFalsy(); - expect( - appTree.exists(`${projectDirectory}/tests/test_hello.py`), - ).toBeFalsy(); + assertGeneratedFilesBase(appTree, 'apps/test', 'test'); + assertGeneratedFilesFlake8(appTree, 'apps/test'); + assertGeneratedFilesPyTest(appTree, 'apps/test'); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); + }); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); + it('should run successfully with ruff linter and pytest with no reports', async () => { + await generator(appTree, { + ...options, + linter: 'ruff', + unitTestRunner: 'pytest', + }); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toMatchSnapshot(); - expect(spawn.sync).toHaveBeenCalledTimes(2); - expect(spawn.sync).toHaveBeenNthCalledWith(1, 'python', ['--version'], { - stdio: 'pipe', - }); - expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['sync'], { - shell: false, - stdio: 'inherit', + assertGeneratedFilesBase(appTree, 'apps/test', 'test'); + assertGeneratedFilesPyTest(appTree, 'apps/test'); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); }); - }); - it('should run successfully minimal configuration as a library', async () => { - await generator(appTree, { - ...options, - projectType: 'library', + it('should run successfully with flake8 linter and pytest with html coverage report', async () => { + await generator(appTree, { + ...options, + linter: 'flake8', + unitTestRunner: 'pytest', + codeCoverage: true, + codeCoverageHtmlReport: true, + }); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toMatchSnapshot(); + + assertGeneratedFilesBase(appTree, 'apps/test', 'test'); + assertGeneratedFilesFlake8(appTree, 'apps/test'); + assertGeneratedFilesPyTest(appTree, 'apps/test'); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); }); - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toMatchSnapshot(); - const projectDirectory = 'libs/test'; - const moduleName = 'test'; + it('should run successfully with flake8 linter and pytest with html,xml coverage reports', async () => { + await generator(appTree, { + ...options, + linter: 'flake8', + unitTestRunner: 'pytest', + codeCoverage: true, + codeCoverageHtmlReport: true, + codeCoverageXmlReport: true, + }); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toMatchSnapshot(); - assertGeneratedFilesBase(appTree, projectDirectory, moduleName); + assertGeneratedFilesBase(appTree, 'apps/test', 'test'); + assertGeneratedFilesFlake8(appTree, 'apps/test'); + assertGeneratedFilesPyTest(appTree, 'apps/test'); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); + }); - expect(appTree.exists(`${projectDirectory}/.flake8`)).toBeFalsy(); - expect( - appTree.exists(`${projectDirectory}/tests/test_hello.py`), - ).toBeFalsy(); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - }); + it('should run successfully with flake8 linter and pytest with html,xml coverage reports and threshold', async () => { + await generator(appTree, { + ...options, + linter: 'flake8', + unitTestRunner: 'pytest', + codeCoverage: true, + codeCoverageHtmlReport: true, + codeCoverageXmlReport: true, + codeCoverageThreshold: 100, + }); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toMatchSnapshot(); - it('should run successfully minimal configuration with tags', async () => { - await generator(appTree, { - ...options, - tags: 'one,two', + assertGeneratedFilesBase(appTree, 'apps/test', 'test'); + assertGeneratedFilesFlake8(appTree, 'apps/test'); + assertGeneratedFilesPyTest(appTree, 'apps/test'); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); }); - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toMatchSnapshot(); - const projectDirectory = 'apps/test'; - const moduleName = 'test'; + it('should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit report', async () => { + await generator(appTree, { + ...options, + linter: 'flake8', + unitTestRunner: 'pytest', + codeCoverage: true, + codeCoverageHtmlReport: true, + codeCoverageXmlReport: true, + codeCoverageThreshold: 100, + unitTestJUnitReport: true, + }); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toMatchSnapshot(); - assertGeneratedFilesBase(appTree, projectDirectory, moduleName); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - }); + assertGeneratedFilesBase(appTree, 'apps/test', 'test'); + assertGeneratedFilesFlake8(appTree, 'apps/test'); + assertGeneratedFilesPyTest(appTree, 'apps/test'); - it('should run successfully minimal configuration custom directory', async () => { - await generator(appTree, { - ...options, - directory: 'subdir', + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); }); - const config = readProjectConfiguration(appTree, 'subdir-test'); - expect(config).toMatchSnapshot(); - const projectDirectory = 'apps/subdir/test'; - const moduleName = 'subdir_test'; + it('should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit,html report', async () => { + await generator(appTree, { + ...options, + linter: 'flake8', + unitTestRunner: 'pytest', + codeCoverage: true, + codeCoverageHtmlReport: true, + codeCoverageXmlReport: true, + codeCoverageThreshold: 100, + unitTestJUnitReport: true, + unitTestHtmlReport: true, + }); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toMatchSnapshot(); - assertGeneratedFilesBase(appTree, projectDirectory, moduleName); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - }); + assertGeneratedFilesBase(appTree, 'apps/test', 'test'); + assertGeneratedFilesFlake8(appTree, 'apps/test'); + assertGeneratedFilesPyTest(appTree, 'apps/test'); - it('should run successfully with flake8 linter', async () => { - await generator(appTree, { - ...options, - linter: 'flake8', + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); }); - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toMatchSnapshot(); - assertGeneratedFilesBase(appTree, 'apps/test', 'test'); - assertGeneratedFilesFlake8(appTree, 'apps/test'); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - }); + it('should run successfully with linting (flake8) and testing options with a dev dependency project', async () => { + await generator(appTree, { + ...options, + projectType: 'library', + name: 'dev-lib', + directory: 'shared', + }); - it('should run successfully with ruff linter', async () => { - await generator(appTree, { - ...options, - linter: 'ruff', - }); - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toMatchSnapshot(); + await generator(appTree, { + ...options, + linter: 'flake8', + unitTestRunner: 'pytest', + codeCoverage: true, + codeCoverageHtmlReport: true, + codeCoverageXmlReport: true, + codeCoverageThreshold: 100, + unitTestJUnitReport: true, + unitTestHtmlReport: true, + devDependenciesProject: 'shared-dev-lib', + }); - assertGeneratedFilesBase(appTree, 'apps/test', 'test'); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - }); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toMatchSnapshot(); - it('should run successfully with flake8 linter and pytest with no reports', async () => { - await generator(appTree, { - ...options, - linter: 'flake8', - unitTestRunner: 'pytest', - }); - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toMatchSnapshot(); + assertGeneratedFilesBase(appTree, 'apps/test', 'test'); + assertGeneratedFilesFlake8(appTree, 'apps/test'); + assertGeneratedFilesPyTest(appTree, 'apps/test'); - assertGeneratedFilesBase(appTree, 'apps/test', 'test'); - assertGeneratedFilesFlake8(appTree, 'apps/test'); - assertGeneratedFilesPyTest(appTree, 'apps/test'); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - }); + assertGeneratedFilesBase( + appTree, + 'libs/shared/dev-lib', + 'shared_dev_lib', + ); - it('should run successfully with ruff linter and pytest with no reports', async () => { - await generator(appTree, { - ...options, - linter: 'ruff', - unitTestRunner: 'pytest', + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); }); - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toMatchSnapshot(); - assertGeneratedFilesBase(appTree, 'apps/test', 'test'); - assertGeneratedFilesPyTest(appTree, 'apps/test'); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - }); + it('should run successfully with linting (ruff) and testing options with a dev dependency project', async () => { + await generator(appTree, { + ...options, + projectType: 'library', + name: 'dev-lib', + directory: 'shared', + }); - it('should run successfully with flake8 linter and pytest with html coverage report', async () => { - await generator(appTree, { - ...options, - linter: 'flake8', - unitTestRunner: 'pytest', - codeCoverage: true, - codeCoverageHtmlReport: true, - }); - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toMatchSnapshot(); + await generator(appTree, { + ...options, + linter: 'ruff', + unitTestRunner: 'pytest', + codeCoverage: true, + codeCoverageHtmlReport: true, + codeCoverageXmlReport: true, + codeCoverageThreshold: 100, + unitTestJUnitReport: true, + unitTestHtmlReport: true, + devDependenciesProject: 'shared-dev-lib', + }); - assertGeneratedFilesBase(appTree, 'apps/test', 'test'); - assertGeneratedFilesFlake8(appTree, 'apps/test'); - assertGeneratedFilesPyTest(appTree, 'apps/test'); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - }); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toMatchSnapshot(); - it('should run successfully with flake8 linter and pytest with html,xml coverage reports', async () => { - await generator(appTree, { - ...options, - linter: 'flake8', - unitTestRunner: 'pytest', - codeCoverage: true, - codeCoverageHtmlReport: true, - codeCoverageXmlReport: true, - }); - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toMatchSnapshot(); + assertGeneratedFilesBase(appTree, 'apps/test', 'test'); + assertGeneratedFilesPyTest(appTree, 'apps/test'); - assertGeneratedFilesBase(appTree, 'apps/test', 'test'); - assertGeneratedFilesFlake8(appTree, 'apps/test'); - assertGeneratedFilesPyTest(appTree, 'apps/test'); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - }); + assertGeneratedFilesBase( + appTree, + 'libs/shared/dev-lib', + 'shared_dev_lib', + ); - it('should run successfully with flake8 linter and pytest with html,xml coverage reports and threshold', async () => { - await generator(appTree, { - ...options, - linter: 'flake8', - unitTestRunner: 'pytest', - codeCoverage: true, - codeCoverageHtmlReport: true, - codeCoverageXmlReport: true, - codeCoverageThreshold: 100, + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); }); - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toMatchSnapshot(); - assertGeneratedFilesBase(appTree, 'apps/test', 'test'); - assertGeneratedFilesFlake8(appTree, 'apps/test'); - assertGeneratedFilesPyTest(appTree, 'apps/test'); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - }); + it('should run successfully with linting and testing options with a dev dependency project with custom package name', async () => { + await generator(appTree, { + ...options, + projectType: 'library', + name: 'dev-lib', + directory: 'shared', + packageName: 'custom-shared-dev-lib', + }); + + await generator(appTree, { + ...options, + linter: 'flake8', + unitTestRunner: 'pytest', + codeCoverage: true, + codeCoverageHtmlReport: true, + codeCoverageXmlReport: true, + codeCoverageThreshold: 100, + unitTestJUnitReport: true, + unitTestHtmlReport: true, + devDependenciesProject: 'shared-dev-lib', + }); - it('should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit report', async () => { - await generator(appTree, { - ...options, - linter: 'flake8', - unitTestRunner: 'pytest', - codeCoverage: true, - codeCoverageHtmlReport: true, - codeCoverageXmlReport: true, - codeCoverageThreshold: 100, - unitTestJUnitReport: true, + expect(appTree.exists(`apps/test/pyproject.toml`)).toBeTruthy(); + expect( + appTree.read(`apps/test/pyproject.toml`, 'utf8'), + ).toMatchSnapshot(); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); }); - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toMatchSnapshot(); - assertGeneratedFilesBase(appTree, 'apps/test', 'test'); - assertGeneratedFilesFlake8(appTree, 'apps/test'); - assertGeneratedFilesPyTest(appTree, 'apps/test'); + it('should run successfully with linting and testing options with an existing dev dependency project', async () => { + await generator(appTree, { + ...options, + projectType: 'library', + name: 'dev-lib', + directory: 'shared', + }); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - }); + const pyprojectToml = parse( + appTree.read('libs/shared/dev-lib/pyproject.toml', 'utf-8'), + ) as UVPyprojectToml; + + pyprojectToml.project.dependencies = [ + 'autopep8>=1.0.0', + 'pytest>=1.0.0', + 'pytest-sugar=>1.0.0', + 'pytest-cov=>1.0.0', + 'pytest-html=>1.0.0', + 'flake8=>1.0.0', + 'flake8-isort=>1.0.0', + ]; + + appTree.write( + 'libs/shared/dev-lib/pyproject.toml', + stringify(pyprojectToml), + ); - it('should run successfully with flake8 linter and pytest with html,xml coverage reports, threshold and junit,html report', async () => { - await generator(appTree, { - ...options, - linter: 'flake8', - unitTestRunner: 'pytest', - codeCoverage: true, - codeCoverageHtmlReport: true, - codeCoverageXmlReport: true, - codeCoverageThreshold: 100, - unitTestJUnitReport: true, - unitTestHtmlReport: true, - }); - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toMatchSnapshot(); + await generator(appTree, { + ...options, + linter: 'flake8', + unitTestRunner: 'pytest', + codeCoverage: true, + codeCoverageHtmlReport: true, + codeCoverageXmlReport: true, + codeCoverageThreshold: 100, + unitTestJUnitReport: true, + unitTestHtmlReport: true, + devDependenciesProject: 'shared-dev-lib', + }); - assertGeneratedFilesBase(appTree, 'apps/test', 'test'); - assertGeneratedFilesFlake8(appTree, 'apps/test'); - assertGeneratedFilesPyTest(appTree, 'apps/test'); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toMatchSnapshot(); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - }); + assertGeneratedFilesBase(appTree, 'apps/test', 'test'); + assertGeneratedFilesFlake8(appTree, 'apps/test'); + assertGeneratedFilesPyTest(appTree, 'apps/test'); - it('should run successfully with linting (flake8) and testing options with a dev dependency project', async () => { - await generator(appTree, { - ...options, - projectType: 'library', - name: 'dev-lib', - directory: 'shared', + assertGeneratedFilesBase( + appTree, + 'libs/shared/dev-lib', + 'shared_dev_lib', + ); }); + }); - await generator(appTree, { - ...options, - linter: 'flake8', - unitTestRunner: 'pytest', - codeCoverage: true, - codeCoverageHtmlReport: true, - codeCoverageXmlReport: true, - codeCoverageThreshold: 100, - unitTestJUnitReport: true, - unitTestHtmlReport: true, - devDependenciesProject: 'shared-dev-lib', - }); + describe('workspace', () => { + it('should run successfully with minimal options with existing custom rootPyprojectDependencyGroup', async () => { + appTree.write( + 'pyproject.toml', + dedent` + [project] + name = "nx-workspace" + version = "1.0.0" + dependencies = [ ] + + [dependency-groups] + dev = [ "requests>=2.3.1" ] + `, + ); + + const callbackTask = await generator(appTree, { + ...options, + rootPyprojectDependencyGroup: 'dev', + }); + callbackTask(); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toMatchSnapshot(); - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toMatchSnapshot(); + const projectDirectory = 'apps/test'; + const moduleName = 'test'; - assertGeneratedFilesBase(appTree, 'apps/test', 'test'); - assertGeneratedFilesFlake8(appTree, 'apps/test'); - assertGeneratedFilesPyTest(appTree, 'apps/test'); + assertGeneratedFilesBase(appTree, projectDirectory, moduleName); - assertGeneratedFilesBase(appTree, 'libs/shared/dev-lib', 'shared_dev_lib'); + expect(appTree.exists(`${projectDirectory}/.flake8`)).toBeFalsy(); + expect( + appTree.exists(`${projectDirectory}/tests/test_hello.py`), + ).toBeFalsy(); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - }); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - it('should run successfully with linting (ruff) and testing options with a dev dependency project', async () => { - await generator(appTree, { - ...options, - projectType: 'library', - name: 'dev-lib', - directory: 'shared', + expect(spawn.sync).toHaveBeenCalledTimes(2); + expect(spawn.sync).toHaveBeenNthCalledWith(1, 'python', ['--version'], { + stdio: 'pipe', + }); + expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['sync'], { + shell: false, + stdio: 'inherit', + }); }); - await generator(appTree, { - ...options, - linter: 'ruff', - unitTestRunner: 'pytest', - codeCoverage: true, - codeCoverageHtmlReport: true, - codeCoverageXmlReport: true, - codeCoverageThreshold: 100, - unitTestJUnitReport: true, - unitTestHtmlReport: true, - devDependenciesProject: 'shared-dev-lib', - }); + it('should run successfully with minimal options without rootPyprojectDependencyGroup', async () => { + appTree.write( + 'pyproject.toml', + dedent` + [project] + name = "nx-workspace" + version = "1.0.0" + dependencies = [ ] + `, + ); - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toMatchSnapshot(); + const callbackTask = await generator(appTree, { + ...options, + rootPyprojectDependencyGroup: undefined, + }); + callbackTask(); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toMatchSnapshot(); - assertGeneratedFilesBase(appTree, 'apps/test', 'test'); - assertGeneratedFilesPyTest(appTree, 'apps/test'); + const projectDirectory = 'apps/test'; + const moduleName = 'test'; - assertGeneratedFilesBase(appTree, 'libs/shared/dev-lib', 'shared_dev_lib'); + assertGeneratedFilesBase(appTree, projectDirectory, moduleName); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - }); + expect(appTree.exists(`${projectDirectory}/.flake8`)).toBeFalsy(); + expect( + appTree.exists(`${projectDirectory}/tests/test_hello.py`), + ).toBeFalsy(); - it('should run successfully with linting and testing options with a dev dependency project with custom package name', async () => { - await generator(appTree, { - ...options, - projectType: 'library', - name: 'dev-lib', - directory: 'shared', - packageName: 'custom-shared-dev-lib', - }); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - await generator(appTree, { - ...options, - linter: 'flake8', - unitTestRunner: 'pytest', - codeCoverage: true, - codeCoverageHtmlReport: true, - codeCoverageXmlReport: true, - codeCoverageThreshold: 100, - unitTestJUnitReport: true, - unitTestHtmlReport: true, - devDependenciesProject: 'shared-dev-lib', + expect(spawn.sync).toHaveBeenCalledTimes(2); + expect(spawn.sync).toHaveBeenNthCalledWith(1, 'python', ['--version'], { + stdio: 'pipe', + }); + expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['sync'], { + shell: false, + stdio: 'inherit', + }); }); - expect(appTree.exists(`apps/test/pyproject.toml`)).toBeTruthy(); - expect(appTree.read(`apps/test/pyproject.toml`, 'utf8')).toMatchSnapshot(); - expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - }); + it('should run successfully with minimal options with custom rootPyprojectDependencyGroup', async () => { + appTree.write( + 'pyproject.toml', + dedent` + [project] + name = "nx-workspace" + version = "1.0.0" + dependencies = [ ] + `, + ); - it('should run successfully with linting and testing options with an existing dev dependency project', async () => { - await generator(appTree, { - ...options, - projectType: 'library', - name: 'dev-lib', - directory: 'shared', - }); + const callbackTask = await generator(appTree, { + ...options, + rootPyprojectDependencyGroup: 'dev', + }); + callbackTask(); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toMatchSnapshot(); - const pyprojectToml = parse( - appTree.read('libs/shared/dev-lib/pyproject.toml', 'utf-8'), - ) as UVPyprojectToml; - - pyprojectToml.project.dependencies = [ - 'autopep8>=1.0.0', - 'pytest>=1.0.0', - 'pytest-sugar=>1.0.0', - 'pytest-cov=>1.0.0', - 'pytest-html=>1.0.0', - 'flake8=>1.0.0', - 'flake8-isort=>1.0.0', - ]; - - appTree.write( - 'libs/shared/dev-lib/pyproject.toml', - stringify(pyprojectToml), - ); - - await generator(appTree, { - ...options, - linter: 'flake8', - unitTestRunner: 'pytest', - codeCoverage: true, - codeCoverageHtmlReport: true, - codeCoverageXmlReport: true, - codeCoverageThreshold: 100, - unitTestJUnitReport: true, - unitTestHtmlReport: true, - devDependenciesProject: 'shared-dev-lib', - }); + const projectDirectory = 'apps/test'; + const moduleName = 'test'; - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toMatchSnapshot(); + assertGeneratedFilesBase(appTree, projectDirectory, moduleName); + + expect(appTree.exists(`${projectDirectory}/.flake8`)).toBeFalsy(); + expect( + appTree.exists(`${projectDirectory}/tests/test_hello.py`), + ).toBeFalsy(); - assertGeneratedFilesBase(appTree, 'apps/test', 'test'); - assertGeneratedFilesFlake8(appTree, 'apps/test'); - assertGeneratedFilesPyTest(appTree, 'apps/test'); + expect(appTree.read('pyproject.toml', 'utf-8')).toMatchSnapshot(); - assertGeneratedFilesBase(appTree, 'libs/shared/dev-lib', 'shared_dev_lib'); + expect(spawn.sync).toHaveBeenCalledTimes(2); + expect(spawn.sync).toHaveBeenNthCalledWith(1, 'python', ['--version'], { + stdio: 'pipe', + }); + expect(spawn.sync).toHaveBeenNthCalledWith(2, 'uv', ['sync'], { + shell: false, + stdio: 'inherit', + }); + }); }); describe('custom template dir', () => { diff --git a/packages/nx-python/src/generators/uv-project/generator.ts b/packages/nx-python/src/generators/uv-project/generator.ts index a9e7e9b..56449b9 100644 --- a/packages/nx-python/src/generators/uv-project/generator.ts +++ b/packages/nx-python/src/generators/uv-project/generator.ts @@ -15,7 +15,6 @@ import _ from 'lodash'; import { UVPyprojectToml } from '../../provider/uv/types'; import { checkUvExecutable, runUv } from '../../provider/uv/utils'; import { DEV_DEPENDENCIES_VERSION_MAP } from '../consts'; -import wcmatch from 'wildcard-match'; import { normalizeOptions as baseNormalizeOptions, getPyprojectTomlByProjectName, @@ -28,6 +27,7 @@ import { interface NormalizedSchema extends BaseNormalizedSchema { devDependenciesProjectPath?: string; devDependenciesProjectPkgName?: string; + individualPackage: boolean; } function normalizeOptions( @@ -59,6 +59,7 @@ function normalizeOptions( devDependenciesProject: options.devDependenciesProject ?? '', devDependenciesProjectPath, devDependenciesProjectPkgName, + individualPackage: !tree.exists('pyproject.toml'), }; } @@ -111,20 +112,13 @@ function updateRootPyprojectToml( tree: Tree, normalizedOptions: NormalizedSchema, ) { - const rootPyprojectToml: UVPyprojectToml = tree.exists('./pyproject.toml') - ? (parse(tree.read('./pyproject.toml', 'utf-8')) as UVPyprojectToml) - : { - project: { name: 'nx-workspace', version: '1.0.0', dependencies: [] }, - 'dependency-groups': {}, - tool: { - uv: { - sources: {}, - workspace: { - members: [`${normalizedOptions.projectRoot.split('/')[0]}/*`], - }, - }, - }, - }; + if (normalizedOptions.individualPackage) { + return; + } + + const rootPyprojectToml: UVPyprojectToml = parse( + tree.read('./pyproject.toml', 'utf-8'), + ) as UVPyprojectToml; const group = normalizedOptions.rootPyprojectDependencyGroup ?? 'main'; @@ -167,28 +161,9 @@ function updateRootPyprojectToml( members: [], }; - if (rootPyprojectToml.tool.uv.workspace.members.length === 0) { - rootPyprojectToml.tool.uv.workspace.members.push( - `${normalizedOptions.projectRoot.split('/')[0]}/*`, - ); - } else { - for (const memberPattern of rootPyprojectToml.tool.uv.workspace.members) { - if ( - !wcmatch( - memberPattern.endsWith('**') - ? memberPattern - : memberPattern.endsWith('*') - ? `${memberPattern}*` - : memberPattern, - )(normalizedOptions.projectRoot) - ) { - rootPyprojectToml.tool.uv.workspace.members.push( - `${normalizedOptions.projectRoot.split('/')[0]}/*`, - ); - } - } - } - + rootPyprojectToml.tool.uv.workspace.members.push( + normalizedOptions.projectRoot, + ); tree.write('./pyproject.toml', stringify(rootPyprojectToml)); } @@ -290,10 +265,12 @@ function addTestDependencies( }; } -function updateRootUvLock() { - console.log(chalk` Updating root {bgBlue uv.lock}...`); - runUv(['sync'], { log: false }); - console.log(chalk`\n {bgBlue uv.lock} updated.\n`); +function updateRootUvLock(tree: Tree) { + if (tree.exists('pyproject.toml')) { + console.log(chalk` Updating root {bgBlue uv.lock}...`); + runUv(['sync'], { log: false }); + console.log(chalk`\n {bgBlue uv.lock} updated.\n`); + } } export default async function ( @@ -334,6 +311,15 @@ export default async function ( bundleLocalDependencies: normalizedOptions.buildBundleLocalDependencies, }, }, + install: { + executor: '@nxlv/python:install', + options: { + silent: false, + args: '', + verbose: false, + debug: false, + }, + }, }; if (options.linter === 'flake8') { @@ -409,6 +395,6 @@ export default async function ( await formatFiles(tree); return () => { - updateRootUvLock(); + updateRootUvLock(tree); }; } diff --git a/packages/nx-python/src/provider/base.ts b/packages/nx-python/src/provider/base.ts index 289fe94..e3dc498 100644 --- a/packages/nx-python/src/provider/base.ts +++ b/packages/nx-python/src/provider/base.ts @@ -94,5 +94,5 @@ export interface IProvider { } & SpawnSyncOptions, ): Promise; - activateVenv(workspaceRoot: string): void; + activateVenv(workspaceRoot: string, context?: ExecutorContext): void; } diff --git a/packages/nx-python/src/provider/poetry/provider.ts b/packages/nx-python/src/provider/poetry/provider.ts index c5bcc99..06d7786 100644 --- a/packages/nx-python/src/provider/poetry/provider.ts +++ b/packages/nx-python/src/provider/poetry/provider.ts @@ -21,7 +21,6 @@ import { addLocalProjectToPoetryProject, checkPoetryExecutable, getAllDependenciesFromPyprojectToml, - getLocalDependencyConfig, getPoetryVersion, getProjectPackageName, getProjectTomlPath, @@ -59,6 +58,7 @@ import { ProjectDependencyResolver, } from './build/resolvers'; import { + getLocalDependencyConfig, getPyprojectData, readPyprojectToml, writePyprojectToml, diff --git a/packages/nx-python/src/provider/poetry/utils.ts b/packages/nx-python/src/provider/poetry/utils.ts index 88d6bfc..dd66338 100644 --- a/packages/nx-python/src/provider/poetry/utils.ts +++ b/packages/nx-python/src/provider/poetry/utils.ts @@ -84,20 +84,6 @@ export function parseToml(tomlFile: string) { return toml.parse(fs.readFileSync(tomlFile, 'utf-8')) as PoetryPyprojectToml; } -export function getLocalDependencyConfig( - context: ExecutorContext, - dependencyName: string, -) { - const dependencyConfig = - context.projectsConfigurations.projects[dependencyName]; - if (!dependencyConfig) { - throw new Error( - chalk`project {bold ${dependencyName}} not found in the Nx workspace`, - ); - } - return dependencyConfig; -} - export type RunPoetryOptions = { log?: boolean; error?: boolean; diff --git a/packages/nx-python/src/provider/resolver.ts b/packages/nx-python/src/provider/resolver.ts index 84c48a0..a50f36a 100644 --- a/packages/nx-python/src/provider/resolver.ts +++ b/packages/nx-python/src/provider/resolver.ts @@ -2,33 +2,77 @@ import fs from 'fs'; import path from 'path'; import { IProvider } from './base'; import { UVProvider } from './uv'; -import { PoetryProvider } from './poetry'; +import { PoetryProvider, PoetryPyprojectToml } from './poetry'; import { Logger } from '../executors/utils/logger'; -import { Tree } from '@nx/devkit'; +import { ExecutorContext, joinPathFragments, Tree } from '@nx/devkit'; +import { getPyprojectData } from './utils'; +import { UVPyprojectToml } from './uv/types'; export const getProvider = async ( workspaceRoot: string, logger?: Logger, tree?: Tree, + context?: ExecutorContext, ): Promise => { const loggerInstance = logger ?? new Logger(); - const uvLockPath = path.join(workspaceRoot, 'uv.lock'); - const poetryLockPath = path.join(workspaceRoot, 'poetry.lock'); - - const isUv = tree ? tree.exists(uvLockPath) : fs.existsSync(uvLockPath); - const isPoetry = tree - ? tree.exists(poetryLockPath) - : fs.existsSync(poetryLockPath); - if (isUv && isPoetry) { + const uv = isUv(workspaceRoot, context, tree); + const poetry = isPoetry(workspaceRoot, context, tree); + if (uv && poetry) { throw new Error( 'Both poetry.lock and uv.lock files found. Please remove one of them.', ); } - if (isUv) { + if (uv) { return new UVProvider(workspaceRoot, loggerInstance, tree); } else { return new PoetryProvider(workspaceRoot, loggerInstance, tree); } }; + +function isUv(workspaceRoot: string, context?: ExecutorContext, tree?: Tree) { + if (context) { + const pyprojectTomlPath = joinPathFragments( + context.projectsConfigurations.projects[context.projectName].root, + 'pyproject.toml', + ); + + if (fs.existsSync(pyprojectTomlPath)) { + const projectData = getPyprojectData< + PoetryPyprojectToml | UVPyprojectToml + >(pyprojectTomlPath); + + return ( + 'project' in projectData && !('poetry' in (projectData.tool ?? {})) + ); + } + } + + const lockPath = path.join(workspaceRoot, 'uv.lock'); + return tree ? tree.exists(lockPath) : fs.existsSync(lockPath); +} + +function isPoetry( + workspaceRoot: string, + context?: ExecutorContext, + tree?: Tree, +) { + if (context) { + const pyprojectTomlPath = joinPathFragments( + context.projectsConfigurations.projects[context.projectName].root, + 'pyproject.toml', + ); + + if (fs.existsSync(pyprojectTomlPath)) { + const projectData = getPyprojectData< + PoetryPyprojectToml | UVPyprojectToml + >(pyprojectTomlPath); + + return 'poetry' in (projectData.tool ?? {}); + } + } + + const lockPath = path.join(workspaceRoot, 'poetry.lock'); + return tree ? tree.exists(lockPath) : fs.existsSync(lockPath); +} diff --git a/packages/nx-python/src/provider/utils.ts b/packages/nx-python/src/provider/utils.ts index 06a4c20..fc0c4f1 100644 --- a/packages/nx-python/src/provider/utils.ts +++ b/packages/nx-python/src/provider/utils.ts @@ -1,5 +1,6 @@ import toml, { JsonMap } from '@iarna/toml'; -import { Tree } from '@nx/devkit'; +import { ExecutorContext, Tree } from '@nx/devkit'; +import chalk from 'chalk'; import { readFileSync } from 'fs'; export const getPyprojectData = (pyprojectToml: string): T => { @@ -29,3 +30,17 @@ export function writePyprojectToml( export function getLoggingTab(level: number): string { return ' '.repeat(level); } + +export function getLocalDependencyConfig( + context: ExecutorContext, + dependencyName: string, +) { + const dependencyConfig = + context.projectsConfigurations.projects[dependencyName]; + if (!dependencyConfig) { + throw new Error( + chalk`project {bold ${dependencyName}} not found in the Nx workspace`, + ); + } + return dependencyConfig; +} diff --git a/packages/nx-python/src/provider/uv/build/resolvers/locked.ts b/packages/nx-python/src/provider/uv/build/resolvers/locked.ts index 68ce676..c3628ab 100644 --- a/packages/nx-python/src/provider/uv/build/resolvers/locked.ts +++ b/packages/nx-python/src/provider/uv/build/resolvers/locked.ts @@ -1,4 +1,4 @@ -import { join } from 'path'; +import path from 'path'; import chalk from 'chalk'; import { Logger } from '../../../../executors/utils/logger'; import { UVPyprojectToml } from '../../types'; @@ -10,7 +10,10 @@ import { includeDependencyPackage } from './utils'; import { existsSync } from 'fs'; export class LockedDependencyResolver { - constructor(private readonly logger: Logger) {} + constructor( + private readonly logger: Logger, + private readonly isWorkspace: boolean, + ) {} public resolve( projectRoot: string, @@ -34,17 +37,20 @@ export class LockedDependencyResolver { continue; } - if (line.startsWith('-e')) { + if (line.startsWith('-e') || line.startsWith('.')) { const location = line.replace('-e', '').trim(); - const dependencyPyprojectPath = join( - workspaceRoot, - location, + const dependencyPath = this.isWorkspace + ? location + : path.relative(process.cwd(), path.resolve(projectRoot, location)); + + const dependencyPyprojectPath = path.join( + dependencyPath, 'pyproject.toml', ); if (!existsSync(dependencyPyprojectPath)) { this.logger.info( - chalk` • Skipping local dependency {blue.bold ${location}} as pyproject.toml not found`, + chalk` • Skipping local dependency {blue.bold ${dependencyPath}} as pyproject.toml not found`, ); continue; } @@ -59,7 +65,7 @@ export class LockedDependencyResolver { includeDependencyPackage( projectData, - location, + dependencyPath, buildFolderPath, buildTomlData, workspaceRoot, diff --git a/packages/nx-python/src/provider/uv/build/resolvers/project.ts b/packages/nx-python/src/provider/uv/build/resolvers/project.ts index f9a86ba..4cfab16 100644 --- a/packages/nx-python/src/provider/uv/build/resolvers/project.ts +++ b/packages/nx-python/src/provider/uv/build/resolvers/project.ts @@ -1,5 +1,5 @@ import chalk from 'chalk'; -import { join, normalize } from 'path'; +import path from 'path'; import { existsSync } from 'fs-extra'; import { UVLockfile, UVPyprojectToml, UVPyprojectTomlIndex } from '../../types'; import { Logger } from '../../../../executors/utils/logger'; @@ -7,15 +7,18 @@ import { PackageDependency } from '../../../base'; import { getLoggingTab, getPyprojectData } from '../../../utils'; import { getUvLockfile } from '../../utils'; import { includeDependencyPackage } from './utils'; -import { BuildExecutorSchema } from '../../../..//executors/build/schema'; +import { BuildExecutorSchema } from '../../../../executors/build/schema'; import { ExecutorContext } from '@nx/devkit'; import { createHash } from 'crypto'; export class ProjectDependencyResolver { + private rootUvLock: UVLockfile | null = null; + constructor( private readonly logger: Logger, private readonly options: BuildExecutorSchema, private readonly context: ExecutorContext, + private readonly isWorkspace: boolean, ) {} resolve( @@ -25,13 +28,12 @@ export class ProjectDependencyResolver { workspaceRoot: string, ): PackageDependency[] { this.logger.info(chalk` Resolving dependencies...`); - const pyprojectPath = join(projectRoot, 'pyproject.toml'); + const pyprojectPath = path.join(projectRoot, 'pyproject.toml'); const projectData = getPyprojectData(pyprojectPath); - const rootUvLook = getUvLockfile(join(workspaceRoot, 'uv.lock')); return this.resolveDependencies( + projectRoot, projectData, - rootUvLook, buildFolderPath, buildTomlData, workspaceRoot, @@ -39,8 +41,8 @@ export class ProjectDependencyResolver { } private resolveDependencies( + projectRoot: string, pyproject: UVPyprojectToml, - rootUvLook: UVLockfile, buildFolderPath: string, buildTomlData: UVPyprojectToml, workspaceRoot: string, @@ -52,12 +54,20 @@ export class ProjectDependencyResolver { for (const dependency of pyproject.project.dependencies) { if (pyproject.tool?.uv?.sources[dependency]) { - const dependencyPath = rootUvLook.package[dependency]?.source?.editable; + const dependencyPath = this.getDependencyPath( + workspaceRoot, + dependency, + projectRoot, + pyproject.tool?.uv?.sources[dependency].path, + ); if (!dependencyPath) { continue; } - const dependencyPyprojectPath = join(dependencyPath, 'pyproject.toml'); + const dependencyPyprojectPath = path.join( + dependencyPath, + 'pyproject.toml', + ); if (!existsSync(dependencyPyprojectPath)) { this.logger.info( chalk`${tab}• Skipping local dependency {blue.bold ${dependency}} as pyproject.toml not found`, @@ -92,8 +102,8 @@ export class ProjectDependencyResolver { ); this.resolveDependencies( + dependencyPath, dependencyPyproject, - rootUvLook, buildFolderPath, buildTomlData, workspaceRoot, @@ -131,6 +141,27 @@ export class ProjectDependencyResolver { return deps; } + private getDependencyPath( + workspaceRoot: string, + dependency: string, + projectRoot: string, + relativePath?: string, + ) { + if (this.isWorkspace) { + if (!this.rootUvLock) { + this.rootUvLock = getUvLockfile(path.join(workspaceRoot, 'uv.lock')); + } + return this.rootUvLock.package[dependency]?.source?.editable; + } else if (relativePath) { + return path.relative( + process.cwd(), + path.resolve(projectRoot, relativePath), + ); + } + + return undefined; + } + private addIndex( buildTomlData: UVPyprojectToml, targetOptions: BuildExecutorSchema, @@ -154,7 +185,7 @@ export class ProjectDependencyResolver { for (const [, config] of Object.entries( this.context.projectsConfigurations.projects, )) { - if (normalize(config.root) === normalize(root)) { + if (path.normalize(config.root) === path.normalize(root)) { return config; } } diff --git a/packages/nx-python/src/provider/uv/provider.ts b/packages/nx-python/src/provider/uv/provider.ts index 6673d14..870d866 100644 --- a/packages/nx-python/src/provider/uv/provider.ts +++ b/packages/nx-python/src/provider/uv/provider.ts @@ -27,6 +27,7 @@ import path, { join } from 'path'; import chalk from 'chalk'; import { copySync, removeSync, writeFileSync } from 'fs-extra'; import { + getLocalDependencyConfig, getPyprojectData, readPyprojectToml, writePyprojectToml, @@ -43,12 +44,18 @@ import { export class UVProvider implements IProvider { protected _rootLockfile: UVLockfile; + protected isWorkspace = false; constructor( - protected workspaceRoot: string, - protected logger: Logger, - protected tree?: Tree, - ) {} + protected readonly workspaceRoot: string, + protected readonly logger: Logger, + protected readonly tree?: Tree, + ) { + const uvLockPath = joinPathFragments(workspaceRoot, 'uv.lock'); + this.isWorkspace = tree + ? tree.exists(uvLockPath) + : fs.existsSync(uvLockPath); + } private get rootLockfile(): UVLockfile { if (!this._rootLockfile) { @@ -165,24 +172,46 @@ export class UVProvider implements IProvider { const result: string[] = []; const { root } = projects[projectName]; + if (this.isWorkspace) { + Object.values(this.rootLockfile.package).forEach((pkg) => { + const deps = [ + ...Object.values(pkg.metadata['requires-dist'] ?? {}), + ...Object.values(pkg.metadata['requires-dev'] ?? {}) + .map((dev) => Object.values(dev)) + .flat(), + ]; + + for (const dep of deps) { + if ( + dep.editable && + path.normalize(dep.editable) === path.normalize(root) + ) { + result.push(pkg.name); + } + } + }); + } else { + const pyprojectToml = getPyprojectData( + joinPathFragments(root, 'pyproject.toml'), + ); - Object.values(this.rootLockfile.package).forEach((pkg) => { - const deps = [ - ...Object.values(pkg.metadata['requires-dist'] ?? {}), - ...Object.values(pkg.metadata['requires-dev'] ?? {}) - .map((dev) => Object.values(dev)) - .flat(), - ]; - - for (const dep of deps) { - if ( - dep.editable && - path.normalize(dep.editable) === path.normalize(root) - ) { - result.push(pkg.name); + for (const project in projects) { + const projectData = projects[project]; + const projectPyprojectTomlPath = joinPathFragments( + projectData.root, + 'pyproject.toml', + ); + if (fs.existsSync(projectPyprojectTomlPath)) { + const tomlData = getPyprojectData( + projectPyprojectTomlPath, + ); + + if (tomlData.tool?.uv?.sources?.[pyprojectToml.project.name]) { + result.push(project); + } } } - }); + } return result; } @@ -192,11 +221,23 @@ export class UVProvider implements IProvider { context: ExecutorContext, ): Promise { await this.checkPrerequisites(); + const projectConfig = + context.projectsConfigurations.projects[context.projectName]; + const projectRoot = projectConfig.root; + + const args = ['add']; + if (!this.isWorkspace && options.local) { + const dependencyConfig = getLocalDependencyConfig(context, options.name); + const dependencyPath = path.relative( + projectConfig.root, + dependencyConfig.root, + ); - const projectRoot = - context.projectsConfigurations.projects[context.projectName].root; + args.push('--editable', dependencyPath); + } else { + args.push(options.name); + } - const args = ['add', options.name, '--project', projectRoot]; if (options.group) { args.push('--group', options.group); } @@ -207,9 +248,20 @@ export class UVProvider implements IProvider { args.push(...(options.args ?? '').split(' ').filter((arg) => !!arg)); - runUv(args, { - cwd: context.root, - }); + if (this.isWorkspace) { + args.push('--project', projectRoot); + runUv(args, { + cwd: context.root, + }); + } else { + runUv(args, { + cwd: projectRoot, + }); + } + + if (!this.isWorkspace) { + this.syncDependents(context, context.projectName); + } } public async update( @@ -217,23 +269,23 @@ export class UVProvider implements IProvider { context: ExecutorContext, ): Promise { await this.checkPrerequisites(); + const projectRoot = this.getProjectRoot(context); - const projectRoot = - context.projectsConfigurations.projects[context.projectName].root; + const args = ['lock', '--upgrade-package', options.name]; + if (this.isWorkspace) { + args.push('--project', projectRoot); + } - const args = [ - 'lock', - '--upgrade-package', - options.name, - '--project', - projectRoot, - ]; runUv(args, { - cwd: context.root, + cwd: this.isWorkspace ? context.root : projectRoot, }); runUv(['sync'], { - cwd: context.root, + cwd: this.isWorkspace ? context.root : projectRoot, }); + + if (!this.isWorkspace) { + this.syncDependents(context, context.projectName); + } } public async remove( @@ -242,14 +294,21 @@ export class UVProvider implements IProvider { ): Promise { await this.checkPrerequisites(); - const projectRoot = - context.projectsConfigurations.projects[context.projectName].root; + const projectRoot = this.getProjectRoot(context); + + const args = ['remove', options.name]; + if (this.isWorkspace) { + args.push('--project', projectRoot); + } - const args = ['remove', options.name, '--project', projectRoot]; args.push(...(options.args ?? '').split(' ').filter((arg) => !!arg)); runUv(args, { - cwd: context.root, + cwd: this.isWorkspace ? context.root : projectRoot, }); + + if (!this.isWorkspace) { + this.syncDependents(context, context.projectName); + } } public async publish( @@ -328,7 +387,7 @@ export class UVProvider implements IProvider { } runUv(args, { - cwd: context.root, + cwd: this.isWorkspace ? context.root : this.getProjectRoot(context), }); } @@ -354,17 +413,16 @@ export class UVProvider implements IProvider { chalk`\n {bold Building project {bgBlue ${context.projectName} }...}\n`, ); - const { root } = - context.projectsConfigurations.projects[context.projectName]; + const projectRoot = this.getProjectRoot(context); const buildFolderPath = join(tmpdir(), 'nx-python', 'build', uuid()); mkdirSync(buildFolderPath, { recursive: true }); this.logger.info(chalk` Copying project files to a temporary folder`); - readdirSync(root).forEach((file) => { + readdirSync(projectRoot).forEach((file) => { if (!options.ignorePaths.includes(file)) { - const source = join(root, file); + const source = join(projectRoot, file); const target = join(buildFolderPath, file); copySync(source, target); } @@ -374,19 +432,19 @@ export class UVProvider implements IProvider { const buildTomlData = getPyprojectData(buildPyProjectToml); const deps = options.lockedVersions - ? new LockedDependencyResolver(this.logger).resolve( - root, + ? new LockedDependencyResolver(this.logger, this.isWorkspace).resolve( + projectRoot, buildFolderPath, buildTomlData, options.devDependencies, context.root, ) - : new ProjectDependencyResolver(this.logger, options, context).resolve( - root, - buildFolderPath, - buildTomlData, - context.root, - ); + : new ProjectDependencyResolver( + this.logger, + options, + context, + this.isWorkspace, + ).resolve(projectRoot, buildFolderPath, buildTomlData, context.root); buildTomlData.project.dependencies = []; buildTomlData['dependency-groups'] = {}; @@ -447,9 +505,18 @@ export class UVProvider implements IProvider { }); } - public activateVenv(workspaceRoot: string): void { + public activateVenv(workspaceRoot: string, context?: ExecutorContext): void { if (!process.env.VIRTUAL_ENV) { - const virtualEnv = path.resolve(workspaceRoot, '.venv'); + if (!this.isWorkspace && !context) { + throw new Error('context is required when not in a workspace'); + } + + const virtualEnv = path.resolve( + this.isWorkspace + ? workspaceRoot + : context.projectsConfigurations.projects[context.projectName].root, + '.venv', + ); process.env.VIRTUAL_ENV = virtualEnv; process.env.PATH = `${virtualEnv}/bin:${process.env.PATH}`; delete process.env.PYTHONHOME; @@ -501,4 +568,34 @@ export class UVProvider implements IProvider { return deps; } + + private getProjectRoot(context: ExecutorContext) { + return context.projectsConfigurations.projects[context.projectName].root; + } + + private syncDependents( + context: ExecutorContext, + projectName: string, + updatedProjects: string[] = [], + ) { + updatedProjects.push(projectName); + const deps = this.getDependents( + projectName, + context.projectsConfigurations.projects, + ); + + for (const dep of deps) { + if (updatedProjects.includes(dep)) { + continue; + } + + this.logger.info(chalk`\nUpdating project {bold ${dep}}`); + const depConfig = context.projectsConfigurations.projects[dep]; + runUv(['sync'], { + cwd: depConfig.root, + }); + + this.syncDependents(context, dep, updatedProjects); + } + } } diff --git a/packages/nx-python/src/provider/uv/types.ts b/packages/nx-python/src/provider/uv/types.ts index a91e656..eff6ddf 100644 --- a/packages/nx-python/src/provider/uv/types.ts +++ b/packages/nx-python/src/provider/uv/types.ts @@ -16,10 +16,14 @@ export type UVPyprojectToml = { }; }; }; + metadata?: { + 'allow-direct-references'?: boolean; + }; }; uv?: { sources?: { [key: string]: { + path?: string; workspace?: boolean; index?: string; }; diff --git a/packages/nx-python/src/provider/uv/utils.ts b/packages/nx-python/src/provider/uv/utils.ts index e76c26d..40598d0 100644 --- a/packages/nx-python/src/provider/uv/utils.ts +++ b/packages/nx-python/src/provider/uv/utils.ts @@ -6,6 +6,7 @@ import { UVLockfile } from './types'; import toml from '@iarna/toml'; import { readFileSync } from 'fs-extra'; import { Tree } from '@nx/devkit'; +import { existsSync } from 'fs'; export const UV_EXECUTABLE = 'uv'; @@ -55,7 +56,17 @@ export function runUv(args: string[], options: RunUvOptions = {}): void { } } -export function getUvLockfile(lockfilePath: string, tree?: Tree): UVLockfile { +export function getUvLockfile( + lockfilePath: string, + tree?: Tree, +): UVLockfile | null { + if (tree && !tree.exists(lockfilePath)) { + return null; + } + if (!tree && !existsSync(lockfilePath)) { + return null; + } + const data = toml.parse( tree ? tree.read(lockfilePath, 'utf-8')