Skip to content

Commit ac6f136

Browse files
authored
feat(lambda-python-alpha): support uv (#33880)
### Issue # (if applicable) Closes #31238 Closes #32413 ### Description of changes - `LambdaFunction` support `uv.lock` - When `uv.lock` detected, install dependencies with `uv pip sync` instead of `pip install` for better performance. But we can further use uv for all locks since export output is always `requirements.txt`. - Dockerfile install uv - Update deprecated runtime in some integs ### Description of how you validated changes Unit + Integ ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 741b0a7 commit ac6f136

File tree

45 files changed

+90238
-30600
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+90238
-30600
lines changed

packages/@aws-cdk/aws-lambda-python-alpha/README.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,22 +65,23 @@ new python.PythonFunction(this, 'MyFunction', {
6565

6666
## Packaging
6767

68-
If `requirements.txt`, `Pipfile` or `poetry.lock` exists at the entry path, the construct will handle installing all required modules in a [Lambda compatible Docker container](https://gallery.ecr.aws/sam/build-python3.7) according to the `runtime` and with the Docker platform based on the target architecture of the Lambda function.
68+
If `requirements.txt`, `Pipfile`, `uv.lock` or `poetry.lock` exists at the entry path, the construct will handle installing all required modules in a [Lambda compatible Docker container](https://gallery.ecr.aws/sam/build-python3.13) according to the `runtime` and with the Docker platform based on the target architecture of the Lambda function.
6969

7070
Python bundles are only recreated and published when a file in a source directory has changed.
7171
Therefore (and as a general best-practice), it is highly recommended to commit a lockfile with a
7272
list of all transitive dependencies and their exact versions. This will ensure that when any dependency version is updated, the bundle asset is recreated and uploaded.
7373

74-
To that end, we recommend using [`pipenv`] or [`poetry`] which have lockfile support.
74+
To that end, we recommend using [`pipenv`], [`uv`] or [`poetry`] which have lockfile support.
7575

7676
- [`pipenv`](https://pipenv-fork.readthedocs.io/en/latest/basics.html#example-pipfile-lock)
7777
- [`poetry`](https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control)
78+
- [`uv`](https://docs.astral.sh/uv/concepts/projects/sync/#exporting-the-lockfile)
7879

7980
Packaging is executed using the `Packaging` class, which:
8081

8182
1. Infers the packaging type based on the files present.
82-
2. If it sees a `Pipfile` or a `poetry.lock` file, it exports it to a compatible `requirements.txt` file with credentials (if they're available in the source files or in the bundling container).
83-
3. Installs dependencies using `pip`.
83+
2. If it sees a `Pipfile`, `uv.lock` or a `poetry.lock` file, it exports it to a compatible `requirements.txt` file with credentials (if they're available in the source files or in the bundling container).
84+
3. Installs dependencies using `pip` or `uv`.
8485
4. Copies the dependencies into an asset that is bundled for the Lambda package.
8586

8687
**Lambda with a requirements.txt**
@@ -109,6 +110,18 @@ Packaging is executed using the `Packaging` class, which:
109110
├── poetry.lock # your poetry lock file has to be present at the entry path
110111
```
111112

113+
**Lambda with a uv.lock**
114+
115+
Reference: https://docs.astral.sh/uv/concepts/projects/layout/
116+
117+
```plaintext
118+
.
119+
├── lambda_function.py # exports a function named 'handler'
120+
├── pyproject.toml # your poetry project definition
121+
├── uv.lock # your uv lock file has to be present at the entry path
122+
├── .python-version # this file is ignored, python version is configured via Runtime
123+
```
124+
112125
**Excluding source files**
113126

114127
You can exclude files from being copied using the optional bundling string array parameter `assetExcludes`:

packages/@aws-cdk/aws-lambda-python-alpha/lib/Dockerfile

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ ARG HTTPS_PROXY
99
# pipenv 2022.4.8 is the last version with Python 3.6 support
1010
ARG PIPENV_VERSION=2022.4.8
1111
ARG POETRY_VERSION=1.5.1
12+
ARG UV_VERSION=0.6.9
1213

1314
# Add virtualenv path
1415
ENV PATH="/usr/app/venv/bin:$PATH"
@@ -19,6 +20,9 @@ ENV PIP_CACHE_DIR=/tmp/pip-cache
1920
# set the poetry cache
2021
ENV POETRY_CACHE_DIR=/tmp/poetry-cache
2122

23+
# set the uv cache
24+
ENV UV_CACHE_DIR=/tmp/uv-cache
25+
2226
RUN \
2327
# create a new virtualenv for python to use
2428
# so that it isn't using root
@@ -33,10 +37,14 @@ RUN \
3337
mkdir /tmp/poetry-cache && \
3438
# Ensure all users can write to poetry cache
3539
chmod -R 777 /tmp/poetry-cache && \
36-
# Install pipenv and poetry
37-
pip install pipenv==$PIPENV_VERSION poetry==$POETRY_VERSION && \
40+
# Create a new location for the uv cache
41+
mkdir /tmp/uv-cache && \
42+
# Ensure all users can write to uv cache
43+
chmod -R 777 /tmp/uv-cache && \
44+
# Install pipenv, poetry and uv
45+
pip install pipenv==$PIPENV_VERSION poetry==$POETRY_VERSION uv==${UV_VERSION} && \
3846
# Ensure no temporary files remain in the caches
39-
rm -rf /tmp/pip-cache/* /tmp/poetry-cache/*
47+
rm -rf /tmp/pip-cache/* /tmp/poetry-cache/* /tmp/uv-cache/*
4048

4149
# Setting a non-root user to run default command,
4250
# This will be overridden later when the Docker container is running, using either the local OS user or props.user.

packages/@aws-cdk/aws-lambda-python-alpha/lib/bundling.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,15 +122,25 @@ export class Bundling implements CdkBundlingOptions {
122122
const packaging = Packaging.fromEntry(options.entry, options.poetryIncludeHashes, options.poetryWithoutUrls);
123123
let bundlingCommands: string[] = [];
124124
bundlingCommands.push(...options.commandHooks?.beforeBundling(options.inputDir, options.outputDir) ?? []);
125-
const exclusionStr = options.assetExcludes?.map(item => `--exclude='${item}'`).join(' ');
125+
126+
const excludes = options.assetExcludes ?? [];
127+
if (packaging.dependenciesFile == DependenciesFile.UV && !excludes.includes('.python-version')) {
128+
excludes.push('.python-version');
129+
}
130+
131+
const exclusionStr = excludes.map(item => `--exclude='${item}'`).join(' ');
126132
bundlingCommands.push([
127133
'rsync', '-rLv', exclusionStr ?? '', `${options.inputDir}/`, options.outputDir,
128134
].filter(item => item).join(' '));
129135
bundlingCommands.push(`cd ${options.outputDir}`);
130136
bundlingCommands.push(packaging.exportCommand ?? '');
131-
if (packaging.dependenciesFile) {
137+
138+
if (packaging.dependenciesFile == DependenciesFile.UV) {
139+
bundlingCommands.push(`uv pip install -r ${DependenciesFile.PIP} --target ${options.outputDir}`);
140+
} else if (packaging.dependenciesFile) {
132141
bundlingCommands.push(`python -m pip install -r ${DependenciesFile.PIP} -t ${options.outputDir}`);
133142
}
143+
134144
bundlingCommands.push(...options.commandHooks?.afterBundling(options.inputDir, options.outputDir) ?? []);
135145
return bundlingCommands;
136146
}

packages/@aws-cdk/aws-lambda-python-alpha/lib/packaging.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export enum DependenciesFile {
55
PIP = 'requirements.txt',
66
POETRY = 'poetry.lock',
77
PIPENV = 'Pipfile.lock',
8+
UV = 'uv.lock',
89
NONE = '',
910
}
1011

@@ -79,6 +80,16 @@ export class Packaging {
7980
});
8081
}
8182

83+
/**
84+
* Packaging with `uv`.
85+
*/
86+
public static withUv() {
87+
return new Packaging({
88+
dependenciesFile: DependenciesFile.UV,
89+
exportCommand: `uv export --frozen --no-emit-workspace --no-dev --no-editable -o ${DependenciesFile.PIP}`,
90+
});
91+
}
92+
8293
/**
8394
* No dependencies or packaging.
8495
*/
@@ -93,6 +104,8 @@ export class Packaging {
93104
return this.withPoetry({ poetryIncludeHashes, poetryWithoutUrls });
94105
} else if (fs.existsSync(path.join(entry, DependenciesFile.PIP))) {
95106
return this.withPip();
107+
} else if (fs.existsSync(path.join(entry, DependenciesFile.UV))) {
108+
return this.withUv();
96109
} else {
97110
return this.withNoPackaging();
98111
}

packages/@aws-cdk/aws-lambda-python-alpha/test/bundling.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,3 +582,29 @@ test('with command hooks', () => {
582582
}),
583583
}));
584584
});
585+
586+
test('Bundling a function with uv dependencies', () => {
587+
const entry = path.join(__dirname, 'lambda-handler-uv');
588+
589+
const assetCode = Bundling.bundle({
590+
entry: path.join(entry, '.'),
591+
runtime: Runtime.PYTHON_3_13,
592+
outputPathSuffix: 'python',
593+
});
594+
595+
expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({
596+
bundling: expect.objectContaining({
597+
command: [
598+
'bash', '-c',
599+
"rsync -rLv --exclude='.python-version' /asset-input/ /asset-output/python && cd /asset-output/python && uv export --frozen --no-emit-workspace --no-dev --no-editable -o requirements.txt && uv pip install -r requirements.txt --target /asset-output/python",
600+
],
601+
}),
602+
}));
603+
604+
const files = fs.readdirSync(assetCode.path);
605+
expect(files).toContain('index.py');
606+
expect(files).toContain('pyproject.toml');
607+
expect(files).toContain('uv.lock');
608+
// Contains hidden files.
609+
expect(files).toContain('.ignorefile');
610+
});

0 commit comments

Comments
 (0)