poise-python 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.kitchen.travis.yml +9 -0
  4. data/.kitchen.yml +8 -0
  5. data/.travis.yml +21 -0
  6. data/.yardopts +7 -0
  7. data/Berksfile +28 -0
  8. data/Gemfile +33 -0
  9. data/LICENSE +201 -0
  10. data/README.md +399 -0
  11. data/Rakefile +17 -0
  12. data/chef/attributes/default.rb +24 -0
  13. data/chef/recipes/default.rb +20 -0
  14. data/lib/poise_python.rb +25 -0
  15. data/lib/poise_python/cheftie.rb +18 -0
  16. data/lib/poise_python/error.rb +23 -0
  17. data/lib/poise_python/python_command_mixin.rb +45 -0
  18. data/lib/poise_python/python_providers.rb +35 -0
  19. data/lib/poise_python/python_providers/base.rb +177 -0
  20. data/lib/poise_python/python_providers/portable_pypy.rb +96 -0
  21. data/lib/poise_python/python_providers/scl.rb +77 -0
  22. data/lib/poise_python/python_providers/system.rb +86 -0
  23. data/lib/poise_python/resources.rb +31 -0
  24. data/lib/poise_python/resources/pip_requirements.rb +102 -0
  25. data/lib/poise_python/resources/python_execute.rb +83 -0
  26. data/lib/poise_python/resources/python_package.rb +322 -0
  27. data/lib/poise_python/resources/python_runtime.rb +114 -0
  28. data/lib/poise_python/resources/python_runtime_pip.rb +167 -0
  29. data/lib/poise_python/resources/python_runtime_test.rb +185 -0
  30. data/lib/poise_python/resources/python_virtualenv.rb +164 -0
  31. data/lib/poise_python/utils.rb +63 -0
  32. data/lib/poise_python/utils/python_encoder.rb +73 -0
  33. data/lib/poise_python/version.rb +20 -0
  34. data/poise-python.gemspec +41 -0
  35. data/test/cookbooks/poise-python_test/metadata.rb +18 -0
  36. data/test/cookbooks/poise-python_test/recipes/default.rb +40 -0
  37. data/test/gemfiles/chef-12.gemfile +19 -0
  38. data/test/gemfiles/master.gemfile +23 -0
  39. data/test/integration/default/serverspec/default_spec.rb +102 -0
  40. data/test/spec/python_command_mixin_spec.rb +115 -0
  41. data/test/spec/python_providers/portable_pypy_spec.rb +68 -0
  42. data/test/spec/python_providers/scl_spec.rb +75 -0
  43. data/test/spec/python_providers/system_spec.rb +81 -0
  44. data/test/spec/resources/pip_requirements_spec.rb +69 -0
  45. data/test/spec/resources/python_package_spec.rb +65 -0
  46. data/test/spec/resources/python_runtime_pip_spec.rb +33 -0
  47. data/test/spec/resources/python_virtualenv_spec.rb +103 -0
  48. data/test/spec/spec_helper.rb +19 -0
  49. data/test/spec/utils/python_encoder_spec.rb +79 -0
  50. data/test/spec/utils_spec.rb +86 -0
  51. metadata +170 -0
@@ -0,0 +1,77 @@
1
+ #
2
+ # Copyright 2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'chef/resource'
18
+ require 'poise_languages'
19
+
20
+ require 'poise_python/error'
21
+ require 'poise_python/python_providers/base'
22
+
23
+
24
+ module PoisePython
25
+ module PythonProviders
26
+ class Scl < Base
27
+ include PoiseLanguages::Scl::Mixin
28
+ provides(:scl)
29
+ scl_package('3.4.2', 'rh-python34', {
30
+ ['redhat', 'centos'] => {
31
+ '~> 7.0' => 'https://www.softwarecollections.org/en/scls/rhscl/rh-python34/epel-7-x86_64/download/rhscl-rh-python34-epel-7-x86_64.noarch.rpm',
32
+ '~> 6.0' => 'https://www.softwarecollections.org/en/scls/rhscl/rh-python34/epel-6-x86_64/download/rhscl-rh-python34-epel-6-x86_64.noarch.rpm',
33
+ },
34
+ })
35
+ scl_package('3.3.2', 'python33', {
36
+ ['redhat', 'centos'] => {
37
+ '~> 7.0' => 'https://www.softwarecollections.org/en/scls/rhscl/python33/epel-7-x86_64/download/rhscl-python33-epel-7-x86_64.noarch.rpm',
38
+ '~> 6.0' => 'https://www.softwarecollections.org/en/scls/rhscl/python33/epel-6-x86_64/download/rhscl-python33-epel-6-x86_64.noarch.rpm',
39
+ },
40
+ 'fedora' => {
41
+ '~> 21.0' => 'https://www.softwarecollections.org/en/scls/rhscl/python33/fedora-21-x86_64/download/rhscl-python33-fedora-21-x86_64.noarch.rpm',
42
+ '~> 20.0' => 'https://www.softwarecollections.org/en/scls/rhscl/python33/fedora-20-x86_64/download/rhscl-python33-fedora-20-x86_64.noarch.rpm',
43
+ },
44
+ })
45
+ scl_package('2.7.8', 'python27', {
46
+ ['redhat', 'centos'] => {
47
+ '~> 7.0' => 'https://www.softwarecollections.org/en/scls/rhscl/python27/epel-7-x86_64/download/rhscl-python27-epel-7-x86_64.noarch.rpm',
48
+ '~> 6.0' => 'https://www.softwarecollections.org/en/scls/rhscl/python27/epel-6-x86_64/download/rhscl-python27-epel-6-x86_64.noarch.rpm',
49
+ },
50
+ 'fedora' => {
51
+ '~> 21.0' => 'https://www.softwarecollections.org/en/scls/rhscl/python27/fedora-21-x86_64/download/rhscl-python27-fedora-21-x86_64.noarch.rpm',
52
+ '~> 20.0' => 'https://www.softwarecollections.org/en/scls/rhscl/python27/fedora-20-x86_64/download/rhscl-python27-fedora-20-x86_64.noarch.rpm',
53
+ },
54
+ })
55
+
56
+ def python_binary
57
+ ::File.join(scl_folder, 'root', 'usr', 'bin', 'python')
58
+ end
59
+
60
+ def python_environment
61
+ scl_environment
62
+ end
63
+
64
+ private
65
+
66
+ def install_python
67
+ install_scl_package
68
+ end
69
+
70
+ def uninstall_python
71
+ uninstall_scl_package
72
+ end
73
+
74
+ end
75
+ end
76
+ end
77
+
@@ -0,0 +1,86 @@
1
+ #
2
+ # Copyright 2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'chef/resource'
18
+ require 'poise_languages'
19
+
20
+ require 'poise_python/error'
21
+ require 'poise_python/python_providers/base'
22
+
23
+
24
+ module PoisePython
25
+ module PythonProviders
26
+ class System < Base
27
+ include PoiseLanguages::System::Mixin
28
+ provides(:system)
29
+ packages('python', {
30
+ debian: {
31
+ '8' => %w{python3.4 python2.7},
32
+ '7' => %w{python3.2 python2.7 python2.6},
33
+ '6' => %w{python3.1 python2.6 python2.5},
34
+ },
35
+ ubuntu: {
36
+ '14.04' => %w{python3.4 python2.7},
37
+ '12.04' => %w{python3.2 python2.7},
38
+ '10.04' => %w{python3.1 python2.6},
39
+ },
40
+ rhel: {default: %w{python}},
41
+ centos: {default: %w{python}},
42
+ fedora: {default: %w{python3 python}},
43
+ amazon: {default: %w{python27 python26 python}},
44
+ })
45
+
46
+ # Output value for the Python binary we are installing. Seems to match
47
+ # package name on all platforms I've checked.
48
+ def python_binary
49
+ ::File.join('', 'usr', 'bin', system_package_name)
50
+ end
51
+
52
+ private
53
+
54
+ def install_python
55
+ install_system_packages
56
+ end
57
+
58
+ def uninstall_python
59
+ uninstall_system_packages
60
+ end
61
+
62
+ def system_package_candidates(version)
63
+ [].tap do |names|
64
+ # For two (or more) digit versions.
65
+ if match = version.match(/^(\d+\.\d+)/)
66
+ # Debian style pythonx.y
67
+ names << "python#{match[1]}"
68
+ # Amazon style pythonxy
69
+ names << "python#{match[1].gsub(/\./, '')}"
70
+ end
71
+ # Aliases for 2 and 3.
72
+ if version == '3' || version == ''
73
+ names.concat(%w{python3.5 python35 python3.4 python34 python3.3 python33 python3.2 python32 python3.1 python31 python3.0 python30 python3})
74
+ end
75
+ if version == '2' || version == ''
76
+ names.concat(%w{python2.7 python27 python2.6 python26 python2.5 python25})
77
+ end
78
+ # For RHEL and friends.
79
+ names << 'python'
80
+ names.uniq!
81
+ end
82
+ end
83
+
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,31 @@
1
+ #
2
+ # Copyright 2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'poise_python/resources/pip_requirements'
18
+ require 'poise_python/resources/python_package'
19
+ require 'poise_python/resources/python_runtime'
20
+ require 'poise_python/resources/python_runtime_pip'
21
+ require 'poise_python/resources/python_execute'
22
+ require 'poise_python/resources/python_virtualenv'
23
+
24
+
25
+ module PoisePython
26
+ # Chef resources and providers for poise-python.
27
+ #
28
+ # @since 1.0.0
29
+ module Resources
30
+ end
31
+ end
@@ -0,0 +1,102 @@
1
+ #
2
+ # Copyright 2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'chef/provider'
18
+ require 'chef/resource'
19
+ require 'poise'
20
+
21
+ require 'poise_python/python_command_mixin'
22
+
23
+
24
+ module PoisePython
25
+ module Resources
26
+ # (see PipRequirements::Resource)
27
+ # @since 1.0.0
28
+ module PipRequirements
29
+ # A `pip_requirements` resource to manage Python virtual environments.
30
+ #
31
+ # @provides pip_requirements
32
+ # @action install
33
+ # @action upgrade
34
+ # @example
35
+ # pip_requirements '/opt/myapp/requirements.txt'
36
+ class Resource < Chef::Resource
37
+ include PoisePython::PythonCommandMixin
38
+ provides(:pip_requirements)
39
+ actions(:install, :upgrade)
40
+
41
+ # @!attribute path
42
+ # Path to the requirements file, or a folder containing the
43
+ # requirements file.
44
+ # @return [String]
45
+ attribute(:path, kind_of: String, name_attribute: true)
46
+ end
47
+
48
+ # The default provider for `pip_requirements`.
49
+ #
50
+ # @see Resource
51
+ # @provides pip_requirements
52
+ class Provider < Chef::Provider
53
+ include Poise
54
+ include PoisePython::PythonCommandMixin
55
+ provides(:pip_requirements)
56
+
57
+ # The `install` action for the `pip_requirements` resource.
58
+ #
59
+ # @return [void]
60
+ def action_install
61
+ install_requirements(upgrade: false)
62
+ end
63
+
64
+ # The `upgrade` action for the `pip_requirements` resource.
65
+ #
66
+ # @return [void]
67
+ def action_upgrade
68
+ install_requirements(upgrade: true)
69
+ end
70
+
71
+ private
72
+
73
+ # Run an install --requirements command and parse the output.
74
+ #
75
+ # @param upgrade [Boolean] If we should use the --upgrade flag.
76
+ # @return [void]
77
+ def install_requirements(upgrade: false)
78
+ cmd = %w{-m pip.__main__ install}
79
+ cmd << '--upgrade' if upgrade
80
+ cmd << '--requirement'
81
+ cmd << requirements_path
82
+ output = python_shell_out!(cmd).stdout
83
+ if output.include?('Successfully installed')
84
+ new_resource.updated_by_last_action(true)
85
+ end
86
+ end
87
+
88
+ # Find the true path to the requirements file.
89
+ #
90
+ # @return [String]
91
+ def requirements_path
92
+ if ::File.directory?(new_resource.path)
93
+ ::File.join(new_resource.path, 'requirements.txt')
94
+ else
95
+ new_resource.path
96
+ end
97
+ end
98
+
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,83 @@
1
+ #
2
+ # Copyright 2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'chef/mixin/which'
18
+ require 'chef/provider/execute'
19
+ require 'chef/resource/execute'
20
+ require 'poise'
21
+
22
+ require 'poise_python/python_command_mixin'
23
+
24
+
25
+ module PoisePython
26
+ module Resources
27
+ # (see PythonExecute::Resource)
28
+ # @since 1.0.0
29
+ module PythonExecute
30
+ # A `python_execute` resource to run Python scripts and commands.
31
+ #
32
+ # @provides python_execute
33
+ # @action run
34
+ # @example
35
+ # python_execute 'myapp.py' do
36
+ # user 'myuser'
37
+ # end
38
+ class Resource < Chef::Resource::Execute
39
+ include PoisePython::PythonCommandMixin
40
+ provides(:python_execute)
41
+ actions(:run)
42
+ end
43
+
44
+ # The default provider for `python_execute`.
45
+ #
46
+ # @see Resource
47
+ # @provides python_execute
48
+ class Provider < Chef::Provider::Execute
49
+ include Chef::Mixin::Which
50
+ provides(:python_execute)
51
+
52
+ private
53
+
54
+ # Command to pass to shell_out.
55
+ #
56
+ # @return [String, Array<String>]
57
+ def command
58
+ if new_resource.command.is_a?(Array)
59
+ [new_resource.python] + new_resource.command
60
+ else
61
+ "#{new_resource.python} #{new_resource.command}"
62
+ end
63
+ end
64
+
65
+ # Environment variables to pass to shell_out.
66
+ #
67
+ # @return [Hash]
68
+ def environment
69
+ if new_resource.parent_python
70
+ environment = new_resource.parent_python.python_environment
71
+ if new_resource.environment
72
+ environment = environment.merge(new_resource.environment)
73
+ end
74
+ environment
75
+ else
76
+ new_resource.environment
77
+ end
78
+ end
79
+
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,322 @@
1
+ #
2
+ # Copyright 2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'shellwords'
18
+
19
+ require 'chef/mixin/which'
20
+ require 'chef/provider/package'
21
+ require 'chef/resource/package'
22
+ require 'poise'
23
+
24
+ require 'poise_python/python_command_mixin'
25
+
26
+
27
+ module PoisePython
28
+ module Resources
29
+ # (see PythonPackage::Resource)
30
+ # @since 1.0.0
31
+ module PythonPackage
32
+ # A Python snippet to hack pip a bit so `pip list --outdated` will show
33
+ # only the things we want and will understand version requirements.
34
+ # @api private
35
+ PIP_HACK_SCRIPT = <<-EOH
36
+ import sys
37
+
38
+ import pip
39
+ try:
40
+ # >= 6.0
41
+ from pip.utils import get_installed_distributions
42
+ except ImportError:
43
+ # <= 1.5.6
44
+ from pip.util import get_installed_distributions
45
+
46
+ def replacement(*args, **kwargs):
47
+ import copy, sys
48
+ from pip._vendor import pkg_resources
49
+ dists = []
50
+ for raw_req in sys.argv[3:]:
51
+ req = pkg_resources.Requirement.parse(raw_req)
52
+ dist = pkg_resources.working_set.by_key.get(req.key)
53
+ if dist:
54
+ # Don't mutate stuff from the global working set.
55
+ dist = copy.copy(dist)
56
+ else:
57
+ # Make a fake one.
58
+ dist = pkg_resources.Distribution(project_name=req.key, version='0')
59
+ # Fool the .key property into using our string.
60
+ dist._key = raw_req
61
+ dists.append(dist)
62
+ return dists
63
+ try:
64
+ # For Python 2.
65
+ get_installed_distributions.func_code = replacement.func_code
66
+ except AttributeError:
67
+ # For Python 3.
68
+ get_installed_distributions.__code__ = replacement.__code__
69
+
70
+ sys.exit(pip.main())
71
+ EOH
72
+
73
+ # A `python_package` resource to manage Python installations using pip.
74
+ #
75
+ # @provides python_package
76
+ # @action install
77
+ # @action upgrade
78
+ # @action uninstall
79
+ # @example
80
+ # python_package 'django' do
81
+ # python '2'
82
+ # version '1.8.3'
83
+ # end
84
+ class Resource < Chef::Resource::Package
85
+ include PoisePython::PythonCommandMixin
86
+ provides(:python_package)
87
+
88
+
89
+ # @!attribute group
90
+ # System group to install the package.
91
+ # @return [String, Integer, nil]
92
+ attribute(:group, kind_of: [String, Integer, NilClass])
93
+ # @!attribute user
94
+ # System user to install the package.
95
+ # @return [String, Integer, nil]
96
+ attribute(:user, kind_of: [String, Integer, NilClass])
97
+
98
+ def initialize(*args)
99
+ super
100
+ # For older Chef.
101
+ @resource_name = :python_package
102
+ # We don't have these actions.
103
+ @allowed_actions.delete(:purge)
104
+ @allowed_actions.delete(:reconfig)
105
+ end
106
+
107
+ # Upstream attribute we don't support. Sets are an error and gets always
108
+ # return nil.
109
+ #
110
+ # @api private
111
+ # @param arg [Object] Ignored
112
+ # @return [nil]
113
+ def response_file(arg=nil)
114
+ raise NoMethodError if arg
115
+ end
116
+
117
+ # (see #response_file)
118
+ def response_file_variables(arg=nil)
119
+ raise NoMethodError if arg
120
+ end
121
+
122
+ # (see #response_file)
123
+ def source(arg=nil)
124
+ raise NoMethodError if arg
125
+ end
126
+ end
127
+
128
+ # The default provider for the `python_package` resource.
129
+ #
130
+ # @see Resource
131
+ class Provider < Chef::Provider::Package
132
+ include PoisePython::PythonCommandMixin
133
+ provides(:python_package)
134
+
135
+ # Load current and candidate versions for all needed packages.
136
+ #
137
+ # @api private
138
+ # @return [Chef::Resource]
139
+ def load_current_resource
140
+ @current_resource = new_resource.class.new(new_resource.name, run_context)
141
+ current_resource.package_name(new_resource.package_name)
142
+ check_package_versions(current_resource)
143
+ current_resource
144
+ end
145
+
146
+ # Populate current and candidate versions for all needed packages.
147
+ #
148
+ # @api private
149
+ # @param resource [PoisePython::Resources::PythonPackage::Resource]
150
+ # Resource to load for.
151
+ # @param version [String, Array<String>] Current version(s) of package(s).
152
+ # @return [void]
153
+ def check_package_versions(resource, version=new_resource.version)
154
+ version_data = Hash.new {|hash, key| hash[key] = {current: nil, candidate: nil} }
155
+ # Get the version for everything currently installed.
156
+ list = pip_command('list').stdout
157
+ parse_pip_list(list).each do |name, current|
158
+ # Merge current versions in to the data.
159
+ version_data[name][:current] = current
160
+ end
161
+ # Check for newer candidates.
162
+ outdated = pip_outdated(pip_requirements(resource.package_name, version)).stdout
163
+ parse_pip_outdated(outdated).each do |name, candidate|
164
+ # Merge candidates in to the existing versions.
165
+ version_data[name][:candidate] = candidate
166
+ end
167
+ # Populate the current resource and candidate versions. Youch this is
168
+ # a gross mix of data flow.
169
+ if(resource.package_name.is_a?(Array))
170
+ @candidate_version = []
171
+ versions = []
172
+ [resource.package_name].flatten.each do |name|
173
+ ver = version_data[name.downcase]
174
+ versions << ver[:current]
175
+ @candidate_version << ver[:candidate]
176
+ end
177
+ resource.version(versions)
178
+ else
179
+ ver = version_data[resource.package_name.downcase]
180
+ resource.version(ver[:current])
181
+ @candidate_version = ver[:candidate]
182
+ end
183
+ end
184
+
185
+ # Install package(s) using pip.
186
+ #
187
+ # @param name [String, Array<String>] Name(s) of package(s).
188
+ # @param version [String, Array<String>] Version(s) of package(s).
189
+ # @return [void]
190
+ def install_package(name, version)
191
+ pip_install(name, version, upgrade: false)
192
+ end
193
+
194
+ # Upgrade package(s) using pip.
195
+ #
196
+ # @param name [String, Array<String>] Name(s) of package(s).
197
+ # @param version [String, Array<String>] Version(s) of package(s).
198
+ # @return [void]
199
+ def upgrade_package(name, version)
200
+ pip_install(name, version, upgrade: true)
201
+ end
202
+
203
+ # Uninstall package(s) using pip.
204
+ #
205
+ # @param name [String, Array<String>] Name(s) of package(s).
206
+ # @param version [String, Array<String>] Version(s) of package(s).
207
+ # @return [void]
208
+ def remove_package(name, version)
209
+ pip_command('uninstall', %w{--yes} + [name].flatten)
210
+ end
211
+
212
+ private
213
+
214
+ # Convert name(s) and version(s) to an array of pkg_resources.Requirement
215
+ # compatible strings. These are strings like "django" or "django==1.0".
216
+ #
217
+ # @param name [String, Array<String>] Name or names for the packages.
218
+ # @param version [String, Array<String>] Version or versions for the
219
+ # packages.
220
+ # @return [Array<String>]
221
+ def pip_requirements(name, version)
222
+ [name].flatten.zip([version].flatten).map do |n, v|
223
+ v = v.to_s.strip
224
+ if v.empty?
225
+ # No version requirement, send through unmodified.
226
+ n
227
+ elsif v =~ /^\d/
228
+ "#{n}==#{v}"
229
+ else
230
+ # If the first character isn't a digit, assume something fancy.
231
+ n + v
232
+ end
233
+ end
234
+ end
235
+
236
+ # Run a pip command.
237
+ #
238
+ # @param pip_command [String] The pip subcommand to run (eg. install).
239
+ # @param pip_options [Array<String>] Options for the pip command.
240
+ # @param opts [Hash] Mixlib::ShellOut options.
241
+ # @return [Mixlib::ShellOut]
242
+ def pip_command(pip_command, pip_options=[], opts={})
243
+ runner = opts.delete(:pip_runner) || %w{-m pip.__main__}
244
+ full_cmd = if new_resource.options
245
+ # We have to use a string for this case to be safe because the
246
+ # options are a string and I don't want to try and parse that.
247
+ "##{runner.join(' ')} #{pip_command} #{new_resource.options} #{Shellwords.join(pip_options)}"
248
+ else
249
+ # No special options, use an array to skip the extra /bin/sh.
250
+ runner + [pip_command] + pip_options
251
+ end
252
+ # Set user and group.
253
+ opts[:user] = new_resource.user if new_resource.user
254
+ opts[:group] = new_resource.group if new_resource.group
255
+
256
+ python_shell_out!(full_cmd, opts)
257
+ end
258
+
259
+ # Run `pip install` to install a package(s).
260
+ #
261
+ # @param name [String, Array<String>] Name(s) of package(s) to install.
262
+ # @param version [String, Array<String>] Version(s) of package(s) to
263
+ # install.
264
+ # @param upgrade [Boolean] Use upgrade mode?
265
+ # @return [Mixlib::ShellOut]
266
+ def pip_install(name, version, upgrade: false)
267
+ cmd = pip_requirements(name, version)
268
+ # Prepend --upgrade if needed.
269
+ cmd = %w{--upgrade} + cmd if upgrade
270
+ pip_command('install', cmd)
271
+ end
272
+
273
+ # Run my hacked version of `pip list --outdated` with a specific set of
274
+ # package requirements.
275
+ #
276
+ # @see #pip_requirements
277
+ # @param requirements [Array<String>] Pip-formatted package requirements.
278
+ # @return [Mixlib::ShellOut]
279
+ def pip_outdated(requirements)
280
+ pip_command('list', %w{--outdated} + requirements, input: PIP_HACK_SCRIPT, pip_runner: %w{-})
281
+ end
282
+
283
+ # Parse the output from `pip list --outdate`. Returns a hash of package
284
+ # key to candidate version.
285
+ #
286
+ # @param text [String] Output to parse.
287
+ # @return [Hash<String, String>]
288
+ def parse_pip_outdated(text)
289
+ text.split(/\n/).inject({}) do |memo, line|
290
+ # Example of a line:
291
+ # boto (Current: 2.25.0 Latest: 2.38.0 [wheel])
292
+ if md = line.match(/^(\S+)\s+\(.*?latest:\s+([^\s,]+).*\)$/i)
293
+ memo[md[1].downcase] = md[2]
294
+ else
295
+ Chef::Log.debug("[#{new_resource}] Unparsable line in pip outdated: #{line}")
296
+ end
297
+ memo
298
+ end
299
+ end
300
+
301
+ # Parse the output from `pip list`. Returns a hash of package key to
302
+ # current version.
303
+ #
304
+ # @param text [String] Output to parse.
305
+ # @return [Hash<String, String>]
306
+ def parse_pip_list(text)
307
+ text.split(/\n/).inject({}) do |memo, line|
308
+ # Example of a line:
309
+ # boto (2.25.0)
310
+ if md = line.match(/^(\S+)\s+\(([^\s,]+).*\)$/i)
311
+ memo[md[1].downcase] = md[2]
312
+ else
313
+ Chef::Log.debug("[#{new_resource}] Unparsable line in pip list: #{line}")
314
+ end
315
+ memo
316
+ end
317
+ end
318
+
319
+ end
320
+ end
321
+ end
322
+ end