Skip to content

Commit 2135228

Browse files
authored
Merge pull request spyder-ide#12924 from goanpeca/enh/add-cookiecutter-widget
PR: Add cookiecutter widget
2 parents fc470b9 + e7f649b commit 2135228

11 files changed

Lines changed: 787 additions & 2 deletions

File tree

.github/scripts/install.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ if [ "$USE_CONDA" = "true" ]; then
1414
fi
1515

1616
# Install main dependencies
17-
conda install python=$PYTHON_VERSION --file requirements/conda.txt -q -y
17+
conda install python=$PYTHON_VERSION --file requirements/conda.txt -q -y -c spyder-ide/label/alpha
1818

1919
# Install test ones
2020
conda install python=$PYTHON_VERSION --file requirements/tests.txt -c spyder-ide -q -y

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ $ workon spyder-dev
4949
After you have created your development environment, you need to install Spyder's necessary dependencies. The easiest way to do so (with Anaconda) is
5050

5151
```bash
52-
$ conda install -c spyder-ide --file requirements/conda.txt
52+
$ conda install -c spyder-ide/label/alpha --file requirements/conda.txt
5353
```
5454

5555
This installs all Spyder's dependencies into the environment.

binder/environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ dependencies:
1111
- atomicwrites >=1.2.0
1212
- chardet >=2.0.0
1313
- cloudpickle >=0.5.0
14+
- cookiecutter >=1.6.0
1415
- diff-match-patch >=20181111
1516
- intervaltree
1617
- ipython >=4.0

requirements/conda.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ applaunchservices >=0.1.7
66
atomicwrites >=1.2.0
77
chardet >=2.0.0
88
cloudpickle >=0.5.0
9+
cookiecutter >=1.6.0
910
diff-match-patch >=20181111
1011
intervaltree
1112
IPython >=4.0

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ def run(self):
216216
'atomicwrites>=1.2.0',
217217
'chardet>=2.0.0',
218218
'cloudpickle>=0.5.0',
219+
'cookiecutter>=1.6.0',
219220
'diff-match-patch>=20181111',
220221
'intervaltree',
221222
'ipython>=4.0',

spyder/dependencies.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
ATOMICWRITES_REQVER = '>=1.2.0'
3434
CHARDET_REQVER = '>=2.0.0'
3535
CLOUDPICKLE_REQVER = '>=0.5.0'
36+
COOKIECUTTER_REQVER = '>=1.6.0'
3637
DIFF_MATCH_PATCH_REQVER = '>=20181111'
3738
INTERVALTREE_REQVER = None
3839
IPYTHON_REQVER = ">=4.0;<6.0" if PY2 else ">=4.0"
@@ -94,6 +95,10 @@
9495
'package_name': "cloudpickle",
9596
'features': _("Handle communications between kernel and frontend"),
9697
'required_version': CLOUDPICKLE_REQVER},
98+
{'modname': "cookiecutter",
99+
'package_name': "cookiecutter",
100+
'features': _("Create projects from cookiecutter templates"),
101+
'required_version': COOKIECUTTER_REQVER},
97102
{'modname': "diff_match_patch",
98103
'package_name': "diff-match-patch",
99104
'features': _("Compute text file diff changes during edition"),
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright © Spyder Project Contributors
4+
# Licensed under the terms of the MIT License
5+
# (see spyder/__init__.py for details)
6+
7+
"""
8+
Cookiecutter utilities.
9+
"""
10+
11+
import json
12+
import os
13+
14+
from cookiecutter.main import cookiecutter
15+
16+
17+
def generate_cookiecutter_project(cookiecutter_path, output_path,
18+
extra_content=None):
19+
"""
20+
Generate a cookicutter project programmatically.
21+
"""
22+
status = True
23+
try:
24+
result = cookiecutter(
25+
cookiecutter_path,
26+
output_dir=output_path,
27+
overwrite_if_exists=True,
28+
extra_context=extra_content,
29+
no_input=True,
30+
)
31+
except Exception as err:
32+
result = err
33+
status = False
34+
35+
return status, result
36+
37+
38+
def load_cookiecutter_project(project_path):
39+
"""
40+
Load a cookicutter options and pre-hook script.
41+
"""
42+
options = None
43+
pre_gen_code = None
44+
cookiepath = os.path.join(project_path, "cookiecutter.json")
45+
pre_gen_path = os.path.join(project_path, "hooks", "pre_gen_project.py")
46+
47+
if os.path.isdir(project_path):
48+
if os.path.isfile(cookiepath):
49+
with open(cookiepath, 'r') as fh:
50+
options = json.loads(fh.read())
51+
52+
if os.path.isfile(pre_gen_path):
53+
with open(pre_gen_path, 'r') as fh:
54+
pre_gen_code = fh.read()
55+
56+
return options, pre_gen_code
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# -*- coding: utf-8 -*-
2+
# -----------------------------------------------------------------------------
3+
# Copyright (c) Spyder Project Contributors
4+
#
5+
# Licensed under the terms of the MIT License
6+
# (see LICENSE.txt for details)
7+
# -----------------------------------------------------------------------------
8+
9+
"""Tests."""
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright © Spyder Project Contributors
4+
# Licensed under the terms of the MIT License
5+
#
6+
7+
"""
8+
Tests for qcookiecutter widget.
9+
"""
10+
11+
# Standard library imports
12+
import json
13+
import os
14+
import shutil
15+
import tempfile
16+
17+
# Third party imports
18+
import pytest
19+
20+
# Local imports
21+
from spyder.plugins.projects.utils.cookie import (
22+
generate_cookiecutter_project, load_cookiecutter_project)
23+
24+
25+
def test_load_cookiecutter_project_config():
26+
settings = {
27+
"opt_1": "value",
28+
"opt_2": "{{ cookiecutter.opt_1 }}",
29+
}
30+
temp_path = tempfile.mkdtemp(suffix='-some-cookiecutter')
31+
temp_cookie_path = os.path.join(temp_path, 'cookiecutter.json')
32+
33+
with open(temp_cookie_path, 'w') as fh:
34+
fh.write(json.dumps(settings, sort_keys=True))
35+
36+
sets, pre_gen_code = load_cookiecutter_project(temp_path)
37+
assert settings == sets
38+
assert pre_gen_code is None
39+
40+
shutil.rmtree(temp_path)
41+
42+
43+
def test_load_cookiecutter_project_hooks():
44+
settings = {
45+
"opt_1": "value",
46+
"opt_2": "{{ cookiecutter.opt_1 }}",
47+
}
48+
pre_gen_code = "import sys\n\nprint('test!')\nsys.exit(1)\n"
49+
temp_path = tempfile.mkdtemp(suffix='-some-cookiecutter')
50+
temp_cookie_path = os.path.join(temp_path, 'cookiecutter.json')
51+
temp_hooks_path = os.path.join(temp_path, 'hooks')
52+
temp_hooks_pre_path = os.path.join(temp_hooks_path, 'pre_gen_project.py')
53+
os.makedirs(temp_hooks_path)
54+
55+
with open(temp_cookie_path, 'w') as fh:
56+
fh.write(json.dumps(settings, sort_keys=True))
57+
58+
with open(temp_hooks_pre_path, 'w') as fh:
59+
fh.write(pre_gen_code)
60+
61+
sets, pre_gen_code = load_cookiecutter_project(temp_path)
62+
assert settings == sets
63+
assert pre_gen_code == pre_gen_code
64+
65+
shutil.rmtree(temp_path)
66+
67+
68+
def test_generate_cookiecutter_project_defaults():
69+
settings = {
70+
"repo_name": "value",
71+
}
72+
temp_path = tempfile.mkdtemp(suffix='-some-cookiecutter')
73+
temp_path_created = tempfile.mkdtemp(suffix='-created-project')
74+
temp_cookie_path = os.path.join(temp_path, 'cookiecutter.json')
75+
temp_project_path = os.path.join(temp_path, '{{cookiecutter.repo_name}}')
76+
os.makedirs(temp_project_path)
77+
78+
with open(temp_cookie_path, 'w') as fh:
79+
fh.write(json.dumps(settings, sort_keys=True))
80+
81+
status, result = generate_cookiecutter_project(
82+
temp_path,
83+
temp_path_created,
84+
)
85+
assert "value" in result
86+
assert status is True
87+
shutil.rmtree(temp_path)
88+
89+
90+
def test_generate_cookiecutter_project_extra_content():
91+
settings = {
92+
"repo_name": "value",
93+
}
94+
temp_path = tempfile.mkdtemp(suffix='-some-cookiecutter')
95+
temp_path_created = tempfile.mkdtemp(suffix='-created-project')
96+
temp_cookie_path = os.path.join(temp_path, 'cookiecutter.json')
97+
temp_project_path = os.path.join(temp_path, '{{cookiecutter.repo_name}}')
98+
os.makedirs(temp_project_path)
99+
100+
with open(temp_cookie_path, 'w') as fh:
101+
fh.write(json.dumps(settings, sort_keys=True))
102+
103+
status, result = generate_cookiecutter_project(
104+
temp_path,
105+
temp_path_created,
106+
{"repo_name": "boom"},
107+
)
108+
assert "boom" in result
109+
assert status is True
110+
shutil.rmtree(temp_path)
111+
112+
113+
def test_generate_cookiecutter_project_exception():
114+
settings = {
115+
"repo_name": "value",
116+
}
117+
temp_path = tempfile.mkdtemp(suffix='-some-invalid-cookiecutter')
118+
temp_path_created = tempfile.mkdtemp(suffix='-created-project')
119+
temp_cookie_path = os.path.join(temp_path, 'cookiecutter.json')
120+
temp_project_path = os.path.join(
121+
temp_path,
122+
'{{cookiecutter.not_foun_variable}}',
123+
)
124+
os.makedirs(temp_project_path)
125+
126+
with open(temp_cookie_path, 'w') as fh:
127+
fh.write(json.dumps(settings, sort_keys=True))
128+
129+
status, __ = generate_cookiecutter_project(
130+
temp_path,
131+
temp_path_created,
132+
)
133+
assert status is False
134+
135+
136+
if __name__ == "__main__":
137+
pytest.main()

0 commit comments

Comments
 (0)