Skip to content

Add support for poetry. #308

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
nodejs 6.16.0
python 3.6.8 2.7.15
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,23 @@ custom:
```


## Poetry support :sparkles::pencil::sparkles:
NOTE: Only poetry version 1 supports the required `export` command for this
feature. As of the point this feature was added, poetry 1.0.0 was in preview
and requires that poetry is installed with the --preview flag.

TL;DR Install poetry with the `--preview` flag.

If you include a `pyproject.toml` and have `poetry` installed instead of a `requirements.txt` this will use
`poetry export --without-hashes -f requirements.txt` to generate them. It is fully compatible with all options such as `zip` and
`dockerizePip`. If you don't want this plugin to generate it for you, set the following option:
```yaml
custom:
pythonRequirements:
usePoetry: false
```


## Dealing with Lambda's size limitations
To help deal with potentially large dependencies (for example: `numpy`, `scipy`
and `scikit-learn`) there is support for compressing the libraries. This does
Expand Down Expand Up @@ -405,3 +422,4 @@ zipinfo .serverless/xxx.zip
* [@alexjurkiewicz](https://github.com/alexjurkiewicz) - [docs about docker workflows](#native-code-dependencies-during-build)
* [@andrewfarley](https://github.com/andrewfarley) - Implemented download caching and static caching
* [@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!
* [@squaresurf](https://github.com/squaresurf) - adding usePoetry option
1 change: 1 addition & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
version: '{build}'
init:
- cmd: pip install pipenv
- cmd: pip install poetry==1.0.0a2
- ps: npm i -g serverless
build: off
test_script:
Expand Down
11 changes: 10 additions & 1 deletion circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ jobs:
- run: sudo apt -y update && sudo apt -y install python-pip python2.7 curl unzip
# instal pipenv
- run: sudo python3.6 -m pip install pipenv pip-tools
# install poetry
- run: |
curl https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py -o get-poetry.py
python get-poetry.py --preview --yes
rm get-poetry.py
# install nodejs
- run: curl -sL https://deb.nodesource.com/setup_6.x | sudo bash - && sudo apt -y install nodejs
# install serverless & depcheck
Expand All @@ -36,4 +41,8 @@ jobs:
# lint:
- run: npm run lint
# test!
- run: npm run test
- run: |
export PATH="$HOME/.poetry/bin:$PATH"
export LC_ALL=C.UTF-8
export LANG=C.UTF-8
npm run test
3 changes: 3 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
const { injectAllRequirements } = require('./lib/inject');
const { installAllRequirements } = require('./lib/pip');
const { pipfileToRequirements } = require('./lib/pipenv');
const { pyprojectTomlToRequirements } = require('./lib/poetry');
const { cleanup, cleanupCache } = require('./lib/clean');

BbPromise.promisifyAll(fse);
Expand All @@ -35,6 +36,7 @@ class ServerlessPythonRequirements {
invalidateCaches: false,
fileName: 'requirements.txt',
usePipenv: true,
usePoetry: true,
pythonBin:
process.platform === 'win32'
? 'python.exe'
Expand Down Expand Up @@ -156,6 +158,7 @@ class ServerlessPythonRequirements {
}
return BbPromise.bind(this)
.then(pipfileToRequirements)
.then(pyprojectTomlToRequirements)
.then(addVendorHelper)
.then(installAllRequirements)
.then(packRequirements);
Expand Down
55 changes: 43 additions & 12 deletions lib/pip.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,22 @@ function generateRequirementsFile(
servicePath,
options
) {
if (options.usePipenv && fse.existsSync(path.join(servicePath, 'Pipfile'))) {
if (
options.usePoetry &&
fse.existsSync(path.join(servicePath, 'pyproject.toml'))
) {
filterRequirementsFile(
path.join(servicePath, '.serverless/requirements.txt'),
targetFile,
options
);
serverless.cli.log(
`Parsed requirements.txt from pyproject.toml in ${targetFile}...`
);
} else if (
options.usePipenv &&
fse.existsSync(path.join(servicePath, 'Pipfile'))
) {
filterRequirementsFile(
path.join(servicePath, '.serverless/requirements.txt'),
targetFile,
Expand Down Expand Up @@ -377,6 +392,31 @@ function copyVendors(vendorFolder, targetFolder, serverless) {
});
}

/**
* This checks if requirements file exists.
* @param {string} servicePath
* @param {Object} options
* @param {string} fileName
*/
function requirementsFileExists(servicePath, options, fileName) {
if (
options.usePoetry &&
fse.existsSync(path.join(servicePath, 'pyproject.toml'))
) {
return true;
}

if (options.usePipenv && fse.existsSync(path.join(servicePath, 'Pipfile'))) {
return true;
}

if (fse.existsSync(fileName)) {
return true;
}

return false;
}

/**
* This evaluates if requirements are actually needed to be installed, but fails
* gracefully if no req file is found intentionally. It also assists with code
Expand All @@ -399,17 +439,8 @@ function installRequirementsIfNeeded(
const fileName = path.join(servicePath, modulePath, options.fileName);

// Skip requirements generation, if requirements file doesn't exist
if (options.usePipenv) {
if (
!fse.existsSync(path.join(servicePath, 'Pipfile')) &&
!fse.existsSync(fileName)
) {
return false;
}
} else {
if (!fse.existsSync(fileName)) {
return false;
}
if (!requirementsFileExists(servicePath, options, fileName)) {
return false;
}

let requirementsTxtDirectory;
Expand Down
43 changes: 43 additions & 0 deletions lib/poetry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const fse = require('fs-extra');
const path = require('path');
const { spawnSync } = require('child_process');

/**
* poetry install
*/
function pyprojectTomlToRequirements() {
if (
!this.options.usePoetry ||
!fse.existsSync(path.join(this.servicePath, 'pyproject.toml'))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one more question. Should this check the contents of the pyproject.toml file since other tools are configured with it too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Do you know what other tools might be configured to use it as well? I thought that file was introduced by poetry.

I did add support to disable this functionality so if someone is using a pyproject.toml without poetry, then they can tell this plugin to ignore the file.

If I were to check the contents, that would most likely add another dep to this plugin so I can parse the toml. Is that something you really want me to do?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know what other tools might be configured to use it as well?

The only tool I know of currently is flit, but I don't think that's for installation, only publication.

I thought that file was introduced by poetry.

Not sure which introduced it, but it's specified in PEP518 and there's nothing stopping someone from making a new packaging tool that uses it

If I were to check the contents, that would most likely add another dep to this plugin so I can parse the toml. Is that something you really want me to do?

fair point. Probably not. Lets leave it like this for now and we can revisit if another package manager leveraging pep518 pops up

) {
return;
}

this.serverless.cli.log('Generating requirements.txt from pyproject.toml...');

const res = spawnSync(
'poetry',
['export', '--without-hashes', '-f', 'requirements.txt'],
{
cwd: this.servicePath
}
);
if (res.error) {
if (res.error.code === 'ENOENT') {
throw new Error(
`poetry not found! Install it according to the poetry docs.`
);
}
throw new Error(res.error);
}
if (res.status !== 0) {
throw new Error(res.stderr);
}
fse.ensureDirSync(path.join(this.servicePath, '.serverless'));
fse.moveSync(
path.join(this.servicePath, 'requirements.txt'),
path.join(this.servicePath, '.serverless', 'requirements.txt')
);
}

module.exports = { pyprojectTomlToRequirements };
117 changes: 115 additions & 2 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const glob = require('glob-all');
const JSZip = require('jszip');
const tape = require('tape');
const {
chmodSync,
removeSync,
readFileSync,
copySync,
Expand Down Expand Up @@ -710,6 +711,94 @@ test("pipenv py3.6 doesn't package bottle with noDeploy option", t => {
t.end();
});

test('poetry py3.6 can package flask with default options', t => {
process.chdir('tests/poetry');
const path = npm(['pack', '../..']);
npm(['i', path]);
sls(['package']);
const zipfiles = listZipFiles('.serverless/sls-py-req-test.zip');
t.true(zipfiles.includes(`flask${sep}__init__.py`), 'flask is packaged');
t.false(zipfiles.includes(`boto3${sep}__init__.py`), 'boto3 is NOT packaged');
t.end();
});

test('poetry py3.6 can package flask with slim option', t => {
process.chdir('tests/poetry');
const path = npm(['pack', '../..']);
npm(['i', path]);
sls(['--slim=true', 'package']);
const zipfiles = listZipFiles('.serverless/sls-py-req-test.zip');
t.true(zipfiles.includes(`flask${sep}__init__.py`), 'flask is packaged');
t.deepEqual(
zipfiles.filter(filename => filename.endsWith('.pyc')),
[],
'no pyc files packaged'
);
t.true(
zipfiles.filter(filename => filename.endsWith('__main__.py')).length > 0,
'__main__.py files are packaged'
);
t.end();
});

test('poetry py3.6 can package flask with slim & slimPatterns options', t => {
process.chdir('tests/poetry');

copySync('_slimPatterns.yml', 'slimPatterns.yml');
const path = npm(['pack', '../..']);
npm(['i', path]);
sls(['--slim=true', 'package']);
const zipfiles = listZipFiles('.serverless/sls-py-req-test.zip');
t.true(zipfiles.includes(`flask${sep}__init__.py`), 'flask is packaged');
t.deepEqual(
zipfiles.filter(filename => filename.endsWith('.pyc')),
[],
'no pyc files packaged'
);
t.deepEqual(
zipfiles.filter(filename => filename.endsWith('__main__.py')),
[],
'__main__.py files are NOT packaged'
);
t.end();
});

test('poetry py3.6 can package flask with zip option', t => {
process.chdir('tests/poetry');
const path = npm(['pack', '../..']);
npm(['i', path]);
sls([`--pythonBin=${getPythonBin(3)}`, '--zip=true', 'package']);
const zipfiles = listZipFiles('.serverless/sls-py-req-test.zip');
t.true(
zipfiles.includes('.requirements.zip'),
'zipped requirements are packaged'
);
t.true(zipfiles.includes(`unzip_requirements.py`), 'unzip util is packaged');
t.false(
zipfiles.includes(`flask${sep}__init__.py`),
"flask isn't packaged on its own"
);
t.end();
});

test("poetry py3.6 doesn't package bottle with noDeploy option", t => {
process.chdir('tests/poetry');
const path = npm(['pack', '../..']);
npm(['i', path]);
perl([
'-p',
'-i.bak',
'-e',
's/(pythonRequirements:$)/\\1\\n noDeploy: [bottle]/',
'serverless.yml'
]);
sls(['package']);
const zipfiles = listZipFiles('.serverless/sls-py-req-test.zip');
t.true(zipfiles.includes(`flask${sep}__init__.py`), 'flask is packaged');
t.false(zipfiles.includes(`bottle.py`), 'bottle is NOT packaged');
t.end();
});

test('py3.6 can package flask with zip option and no explicit include', t => {
process.chdir('tests/base');
const path = npm(['pack', '../..']);
Expand Down Expand Up @@ -760,7 +849,8 @@ test(
's/(handler.py.*$)/$1\n - foobar/',
'serverless.yml'
]);
writeFileSync(`foobar`, '', { mode: perm });
writeFileSync(`foobar`, '');
chmodSync(`foobar`, perm);
sls(['--vendor=./vendor', 'package']);

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

test('poetry py3.6 can package flask with slim & slimPatterns & slimPatternsAppendDefaults=false option', t => {
process.chdir('tests/poetry');
copySync('_slimPatterns.yml', 'slimPatterns.yml');
const path = npm(['pack', '../..']);
npm(['i', path]);

sls(['--slim=true', '--slimPatternsAppendDefaults=false', 'package']);
const zipfiles = listZipFiles('.serverless/sls-py-req-test.zip');
t.true(zipfiles.includes(`flask${sep}__init__.py`), 'flask is packaged');
t.true(
zipfiles.filter(filename => filename.endsWith('.pyc')).length >= 1,
'pyc files are packaged'
);
t.deepEqual(
zipfiles.filter(filename => filename.endsWith('__main__.py')),
[],
'__main__.py files are NOT packaged'
);
t.end();
});

test('py3.6 can package flask with package individually option', t => {
process.chdir('tests/base');
const path = npm(['pack', '../..']);
Expand Down Expand Up @@ -1449,7 +1560,8 @@ test(
process.chdir('tests/individually');
const path = npm(['pack', '../..']);
const perm = '775';
writeFileSync(`module1${sep}foobar`, '', { mode: perm });
writeFileSync(`module1${sep}foobar`, '');
chmodSync(`module1${sep}foobar`, perm);

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

npm(['i', path]);
sls(['--dockerizePip=true', 'package']);
Expand Down
22 changes: 22 additions & 0 deletions tests/poetry/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# Serverless
.serverless
.requirements
unzip_requirements.py
2 changes: 2 additions & 0 deletions tests/poetry/_slimPatterns.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
slimPatterns:
- "**/__main__.py"
5 changes: 5 additions & 0 deletions tests/poetry/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import requests


def hello(event, context):
return requests.get('https://httpbin.org/get').json()
Loading