Skip to content

Commit 2756915

Browse files
authored
Merge pull request #308 from squaresurf/poetry
Add support for poetry.
2 parents 190df96 + 60a3895 commit 2756915

15 files changed

+518
-15
lines changed

.tool-versions

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
nodejs 6.16.0
2+
python 3.6.8 2.7.15

README.md

+18
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,23 @@ custom:
9494
```
9595

9696

97+
## Poetry support :sparkles::pencil::sparkles:
98+
NOTE: Only poetry version 1 supports the required `export` command for this
99+
feature. As of the point this feature was added, poetry 1.0.0 was in preview
100+
and requires that poetry is installed with the --preview flag.
101+
102+
TL;DR Install poetry with the `--preview` flag.
103+
104+
If you include a `pyproject.toml` and have `poetry` installed instead of a `requirements.txt` this will use
105+
`poetry export --without-hashes -f requirements.txt` to generate them. It is fully compatible with all options such as `zip` and
106+
`dockerizePip`. If you don't want this plugin to generate it for you, set the following option:
107+
```yaml
108+
custom:
109+
pythonRequirements:
110+
usePoetry: false
111+
```
112+
113+
97114
## Dealing with Lambda's size limitations
98115
To help deal with potentially large dependencies (for example: `numpy`, `scipy`
99116
and `scikit-learn`) there is support for compressing the libraries. This does
@@ -405,3 +422,4 @@ zipinfo .serverless/xxx.zip
405422
* [@alexjurkiewicz](https://github.com/alexjurkiewicz) - [docs about docker workflows](#native-code-dependencies-during-build)
406423
* [@andrewfarley](https://github.com/andrewfarley) - Implemented download caching and static caching
407424
* [@bweigel](https://github.com/bweigel) - adding the `slimPatternsAppendDefaults` option & fixing per-function packaging when some functions don't have requirements & Porting tests from bats to js!
425+
* [@squaresurf](https://github.com/squaresurf) - adding usePoetry option

appveyor.yml

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
version: '{build}'
22
init:
33
- cmd: pip install pipenv
4+
- cmd: pip install poetry==1.0.0a2
45
- ps: npm i -g serverless
56
build: off
67
test_script:

circle.yml

+10-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ jobs:
2525
- run: sudo apt -y update && sudo apt -y install python-pip python2.7 curl unzip
2626
# instal pipenv
2727
- run: sudo python3.6 -m pip install pipenv pip-tools
28+
# install poetry
29+
- run: |
30+
curl https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py -o get-poetry.py
31+
python get-poetry.py --preview --yes
32+
rm get-poetry.py
2833
# install nodejs
2934
- run: curl -sL https://deb.nodesource.com/setup_6.x | sudo bash - && sudo apt -y install nodejs
3035
# install serverless & depcheck
@@ -36,4 +41,8 @@ jobs:
3641
# lint:
3742
- run: npm run lint
3843
# test!
39-
- run: npm run test
44+
- run: |
45+
export PATH="$HOME/.poetry/bin:$PATH"
46+
export LC_ALL=C.UTF-8
47+
export LANG=C.UTF-8
48+
npm run test

index.js

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
const { injectAllRequirements } = require('./lib/inject');
1313
const { installAllRequirements } = require('./lib/pip');
1414
const { pipfileToRequirements } = require('./lib/pipenv');
15+
const { pyprojectTomlToRequirements } = require('./lib/poetry');
1516
const { cleanup, cleanupCache } = require('./lib/clean');
1617

1718
BbPromise.promisifyAll(fse);
@@ -35,6 +36,7 @@ class ServerlessPythonRequirements {
3536
invalidateCaches: false,
3637
fileName: 'requirements.txt',
3738
usePipenv: true,
39+
usePoetry: true,
3840
pythonBin:
3941
process.platform === 'win32'
4042
? 'python.exe'
@@ -156,6 +158,7 @@ class ServerlessPythonRequirements {
156158
}
157159
return BbPromise.bind(this)
158160
.then(pipfileToRequirements)
161+
.then(pyprojectTomlToRequirements)
159162
.then(addVendorHelper)
160163
.then(installAllRequirements)
161164
.then(packRequirements);

lib/pip.js

+43-12
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,22 @@ function generateRequirementsFile(
6060
servicePath,
6161
options
6262
) {
63-
if (options.usePipenv && fse.existsSync(path.join(servicePath, 'Pipfile'))) {
63+
if (
64+
options.usePoetry &&
65+
fse.existsSync(path.join(servicePath, 'pyproject.toml'))
66+
) {
67+
filterRequirementsFile(
68+
path.join(servicePath, '.serverless/requirements.txt'),
69+
targetFile,
70+
options
71+
);
72+
serverless.cli.log(
73+
`Parsed requirements.txt from pyproject.toml in ${targetFile}...`
74+
);
75+
} else if (
76+
options.usePipenv &&
77+
fse.existsSync(path.join(servicePath, 'Pipfile'))
78+
) {
6479
filterRequirementsFile(
6580
path.join(servicePath, '.serverless/requirements.txt'),
6681
targetFile,
@@ -377,6 +392,31 @@ function copyVendors(vendorFolder, targetFolder, serverless) {
377392
});
378393
}
379394

395+
/**
396+
* This checks if requirements file exists.
397+
* @param {string} servicePath
398+
* @param {Object} options
399+
* @param {string} fileName
400+
*/
401+
function requirementsFileExists(servicePath, options, fileName) {
402+
if (
403+
options.usePoetry &&
404+
fse.existsSync(path.join(servicePath, 'pyproject.toml'))
405+
) {
406+
return true;
407+
}
408+
409+
if (options.usePipenv && fse.existsSync(path.join(servicePath, 'Pipfile'))) {
410+
return true;
411+
}
412+
413+
if (fse.existsSync(fileName)) {
414+
return true;
415+
}
416+
417+
return false;
418+
}
419+
380420
/**
381421
* This evaluates if requirements are actually needed to be installed, but fails
382422
* gracefully if no req file is found intentionally. It also assists with code
@@ -399,17 +439,8 @@ function installRequirementsIfNeeded(
399439
const fileName = path.join(servicePath, modulePath, options.fileName);
400440

401441
// Skip requirements generation, if requirements file doesn't exist
402-
if (options.usePipenv) {
403-
if (
404-
!fse.existsSync(path.join(servicePath, 'Pipfile')) &&
405-
!fse.existsSync(fileName)
406-
) {
407-
return false;
408-
}
409-
} else {
410-
if (!fse.existsSync(fileName)) {
411-
return false;
412-
}
442+
if (!requirementsFileExists(servicePath, options, fileName)) {
443+
return false;
413444
}
414445

415446
let requirementsTxtDirectory;

lib/poetry.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
const fse = require('fs-extra');
2+
const path = require('path');
3+
const { spawnSync } = require('child_process');
4+
5+
/**
6+
* poetry install
7+
*/
8+
function pyprojectTomlToRequirements() {
9+
if (
10+
!this.options.usePoetry ||
11+
!fse.existsSync(path.join(this.servicePath, 'pyproject.toml'))
12+
) {
13+
return;
14+
}
15+
16+
this.serverless.cli.log('Generating requirements.txt from pyproject.toml...');
17+
18+
const res = spawnSync(
19+
'poetry',
20+
['export', '--without-hashes', '-f', 'requirements.txt'],
21+
{
22+
cwd: this.servicePath
23+
}
24+
);
25+
if (res.error) {
26+
if (res.error.code === 'ENOENT') {
27+
throw new Error(
28+
`poetry not found! Install it according to the poetry docs.`
29+
);
30+
}
31+
throw new Error(res.error);
32+
}
33+
if (res.status !== 0) {
34+
throw new Error(res.stderr);
35+
}
36+
fse.ensureDirSync(path.join(this.servicePath, '.serverless'));
37+
fse.moveSync(
38+
path.join(this.servicePath, 'requirements.txt'),
39+
path.join(this.servicePath, '.serverless', 'requirements.txt')
40+
);
41+
}
42+
43+
module.exports = { pyprojectTomlToRequirements };

test.js

+115-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const glob = require('glob-all');
44
const JSZip = require('jszip');
55
const tape = require('tape');
66
const {
7+
chmodSync,
78
removeSync,
89
readFileSync,
910
copySync,
@@ -710,6 +711,94 @@ test("pipenv py3.6 doesn't package bottle with noDeploy option", t => {
710711
t.end();
711712
});
712713

714+
test('poetry py3.6 can package flask with default options', t => {
715+
process.chdir('tests/poetry');
716+
const path = npm(['pack', '../..']);
717+
npm(['i', path]);
718+
sls(['package']);
719+
const zipfiles = listZipFiles('.serverless/sls-py-req-test.zip');
720+
t.true(zipfiles.includes(`flask${sep}__init__.py`), 'flask is packaged');
721+
t.false(zipfiles.includes(`boto3${sep}__init__.py`), 'boto3 is NOT packaged');
722+
t.end();
723+
});
724+
725+
test('poetry py3.6 can package flask with slim option', t => {
726+
process.chdir('tests/poetry');
727+
const path = npm(['pack', '../..']);
728+
npm(['i', path]);
729+
sls(['--slim=true', 'package']);
730+
const zipfiles = listZipFiles('.serverless/sls-py-req-test.zip');
731+
t.true(zipfiles.includes(`flask${sep}__init__.py`), 'flask is packaged');
732+
t.deepEqual(
733+
zipfiles.filter(filename => filename.endsWith('.pyc')),
734+
[],
735+
'no pyc files packaged'
736+
);
737+
t.true(
738+
zipfiles.filter(filename => filename.endsWith('__main__.py')).length > 0,
739+
'__main__.py files are packaged'
740+
);
741+
t.end();
742+
});
743+
744+
test('poetry py3.6 can package flask with slim & slimPatterns options', t => {
745+
process.chdir('tests/poetry');
746+
747+
copySync('_slimPatterns.yml', 'slimPatterns.yml');
748+
const path = npm(['pack', '../..']);
749+
npm(['i', path]);
750+
sls(['--slim=true', 'package']);
751+
const zipfiles = listZipFiles('.serverless/sls-py-req-test.zip');
752+
t.true(zipfiles.includes(`flask${sep}__init__.py`), 'flask is packaged');
753+
t.deepEqual(
754+
zipfiles.filter(filename => filename.endsWith('.pyc')),
755+
[],
756+
'no pyc files packaged'
757+
);
758+
t.deepEqual(
759+
zipfiles.filter(filename => filename.endsWith('__main__.py')),
760+
[],
761+
'__main__.py files are NOT packaged'
762+
);
763+
t.end();
764+
});
765+
766+
test('poetry py3.6 can package flask with zip option', t => {
767+
process.chdir('tests/poetry');
768+
const path = npm(['pack', '../..']);
769+
npm(['i', path]);
770+
sls([`--pythonBin=${getPythonBin(3)}`, '--zip=true', 'package']);
771+
const zipfiles = listZipFiles('.serverless/sls-py-req-test.zip');
772+
t.true(
773+
zipfiles.includes('.requirements.zip'),
774+
'zipped requirements are packaged'
775+
);
776+
t.true(zipfiles.includes(`unzip_requirements.py`), 'unzip util is packaged');
777+
t.false(
778+
zipfiles.includes(`flask${sep}__init__.py`),
779+
"flask isn't packaged on its own"
780+
);
781+
t.end();
782+
});
783+
784+
test("poetry py3.6 doesn't package bottle with noDeploy option", t => {
785+
process.chdir('tests/poetry');
786+
const path = npm(['pack', '../..']);
787+
npm(['i', path]);
788+
perl([
789+
'-p',
790+
'-i.bak',
791+
'-e',
792+
's/(pythonRequirements:$)/\\1\\n noDeploy: [bottle]/',
793+
'serverless.yml'
794+
]);
795+
sls(['package']);
796+
const zipfiles = listZipFiles('.serverless/sls-py-req-test.zip');
797+
t.true(zipfiles.includes(`flask${sep}__init__.py`), 'flask is packaged');
798+
t.false(zipfiles.includes(`bottle.py`), 'bottle is NOT packaged');
799+
t.end();
800+
});
801+
713802
test('py3.6 can package flask with zip option and no explicit include', t => {
714803
process.chdir('tests/base');
715804
const path = npm(['pack', '../..']);
@@ -760,7 +849,8 @@ test(
760849
's/(handler.py.*$)/$1\n - foobar/',
761850
'serverless.yml'
762851
]);
763-
writeFileSync(`foobar`, '', { mode: perm });
852+
writeFileSync(`foobar`, '');
853+
chmodSync(`foobar`, perm);
764854
sls(['--vendor=./vendor', 'package']);
765855

766856
const zipfiles = listZipFiles('.serverless/sls-py-req-test.zip');
@@ -1001,6 +1091,27 @@ test('pipenv py3.6 can package flask with slim & slimPatterns & slimPatternsAppe
10011091
t.end();
10021092
});
10031093

1094+
test('poetry py3.6 can package flask with slim & slimPatterns & slimPatternsAppendDefaults=false option', t => {
1095+
process.chdir('tests/poetry');
1096+
copySync('_slimPatterns.yml', 'slimPatterns.yml');
1097+
const path = npm(['pack', '../..']);
1098+
npm(['i', path]);
1099+
1100+
sls(['--slim=true', '--slimPatternsAppendDefaults=false', 'package']);
1101+
const zipfiles = listZipFiles('.serverless/sls-py-req-test.zip');
1102+
t.true(zipfiles.includes(`flask${sep}__init__.py`), 'flask is packaged');
1103+
t.true(
1104+
zipfiles.filter(filename => filename.endsWith('.pyc')).length >= 1,
1105+
'pyc files are packaged'
1106+
);
1107+
t.deepEqual(
1108+
zipfiles.filter(filename => filename.endsWith('__main__.py')),
1109+
[],
1110+
'__main__.py files are NOT packaged'
1111+
);
1112+
t.end();
1113+
});
1114+
10041115
test('py3.6 can package flask with package individually option', t => {
10051116
process.chdir('tests/base');
10061117
const path = npm(['pack', '../..']);
@@ -1449,7 +1560,8 @@ test(
14491560
process.chdir('tests/individually');
14501561
const path = npm(['pack', '../..']);
14511562
const perm = '775';
1452-
writeFileSync(`module1${sep}foobar`, '', { mode: perm });
1563+
writeFileSync(`module1${sep}foobar`, '');
1564+
chmodSync(`module1${sep}foobar`, perm);
14531565

14541566
npm(['i', path]);
14551567
sls(['package']);
@@ -1475,6 +1587,7 @@ test(
14751587
const path = npm(['pack', '../..']);
14761588
const perm = '775';
14771589
writeFileSync(`module1${sep}foobar`, '', { mode: perm });
1590+
chmodSync(`module1${sep}foobar`, perm);
14781591

14791592
npm(['i', path]);
14801593
sls(['--dockerizePip=true', 'package']);

tests/poetry/.gitignore

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Distribution / packaging
2+
.Python
3+
env/
4+
build/
5+
develop-eggs/
6+
dist/
7+
downloads/
8+
eggs/
9+
.eggs/
10+
lib/
11+
lib64/
12+
parts/
13+
sdist/
14+
var/
15+
*.egg-info/
16+
.installed.cfg
17+
*.egg
18+
19+
# Serverless
20+
.serverless
21+
.requirements
22+
unzip_requirements.py

tests/poetry/_slimPatterns.yml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
slimPatterns:
2+
- "**/__main__.py"

tests/poetry/handler.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import requests
2+
3+
4+
def hello(event, context):
5+
return requests.get('https://httpbin.org/get').json()

0 commit comments

Comments
 (0)