Merge lp:~techalchemy/mojo/python3-fixes into lp:~techalchemy/mojo/python3-updates

Proposed by Dan Ryan
Status: Superseded
Proposed branch: lp:~techalchemy/mojo/python3-fixes
Merge into: lp:~techalchemy/mojo/python3-updates
Diff against target: 1158 lines (+346/-241)
22 files modified
Makefile (+4/-4)
mojo/cli.py (+78/-70)
mojo/contain.py (+0/-1)
mojo/juju/__init__.py (+1/-1)
mojo/juju/check.py (+1/-1)
mojo/juju/checks.py (+1/-1)
mojo/juju/debuglogs.py (+3/-3)
mojo/juju/parse_status.py (+1/-1)
mojo/juju/status.py (+9/-14)
mojo/juju/utils.py (+1/-8)
mojo/juju/wait.py (+3/-0)
mojo/phase.py (+4/-7)
mojo/project.py (+3/-0)
mojo/shutil_which.py (+140/-0)
mojo/tests/test_cli.py (+2/-2)
mojo/tests/test_debuglogs.py (+2/-1)
mojo/tests/test_juju1.py (+7/-3)
mojo/tests/test_juju2.py (+4/-4)
mojo/tests/test_phase.py (+2/-1)
mojo/tests/utils.py (+1/-8)
mojo/utils.py (+77/-110)
setup.py (+2/-1)
To merge this branch: bzr merge lp:~techalchemy/mojo/python3-fixes
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Approve
Review via email: mp+379590@code.launchpad.net

This proposal has been superseded by a proposal from 2020-02-24.

Commit message

- Add six to install_requires
- Drop usage of `distutils.spawn`
- Document modifications to path normalization
- Revert deletion of comment from `juju/wait.py`
- Add module-level attributes with juju path in tests to avoid invoking (likely) non-existent snap
- Fix broken `juju.utils` import in `phase.py`
- Remove extraneous behavior changing error handling
  - Restore 0-indexing of list created by `.split("\n")` rather than invoking `next(iter())`
- Move `get_prog` to argument parser `__init__` method
- Add comments arround error handling in CLI
- Undo accidental formatting changes
- Backport `shutil.which` and supporting functionality for python 2
- Remove global declaration of preferred encoding
- Document `subprocess_run` functionality
- Document `encode_for_stream` function
- Simplify reimplementation of `dist` function as the code is meant to be ubuntu specific
- Fix Makefile variables

To post a comment you must log in.
Revision history for this message
Stuart Bishop (stub) wrote :

Yup, this is looking good.

review: Approve

Unmerged revisions

532. By Dan Ryan

Add six to install_requires

531. By Dan Ryan

- Drop usage of `distutils.spawn`
- Document modifications to path normalization
- Revert deletion of comment from `juju/wait.py`
- Add module-level attributes with juju path in tests to avoid invoking (likely) non-existent snap
- Fix broken `juju.utils` import in `phase.py`
- Remove extraneous behavior changing error handling
- Restore 0-indexing of list created by `.split("\n")` rather than invoking `next(iter())`

530. By Dan Ryan

- Move `get_prog` to argument parser `__init__` method
- Add comments arround error handling in CLI
- Undo accidental formatting changes

529. By Dan Ryan

- Backport `shutil.which` and supporting functionality for python 2
- Remove global declaration of preferred encoding
- Document `subprocess_run` functionality
- Document `encode_for_stream` function
- Simplify reimplementation of `dist` function as the code is meant to be ubuntu specific

528. By Dan Ryan

Fix makefile variables

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2020-02-03 23:44:35 +0000
3+++ Makefile 2020-02-20 21:22:11 +0000
4@@ -7,12 +7,12 @@
5 JUJUCLIENT="lp:python-jujuclient"
6 WEBSOCKETCLIENT="https://github.com/liris/websocket-client.git"
7
8-PYTHON := $(shell /usr/bin/env python3)
9+PYTHON := python3
10 TESTS=mojo/tests/
11 PROJECT=mojo
12 DIST_PACKAGES=python3-setuptools python3-jinja2 lxc git python3-nose juju-deployer juju python3-argcomplete python3-distro-info
13
14-SHELL := $(shell readlink --canonicalize $(shell which bash))
15+SHELL := /bin/bash
16
17 USERINSTALL=--user
18
19@@ -82,10 +82,10 @@
20 build: test lint
21
22 dist:
23- bzr export --root=. ../mojo_$$(python3 setup.py --version).orig.tar.bz2 .
24+ bzr export --root=. ../mojo_$$($(PYTHON) setup.py --version).orig.tar.bz2 .
25
26 dch:
27- dch -D trusty --newversion $$(python3 setup.py -V)
28+ dch -D trusty --newversion $$($(PYTHON) setup.py -V)
29
30 deb: dist dch
31 cd debian && debuild -sa -I.bzr
32
33=== modified file 'mojo/cli.py'
34--- mojo/cli.py 2020-02-06 16:39:27 +0000
35+++ mojo/cli.py 2020-02-20 21:22:11 +0000
36@@ -359,6 +359,25 @@
37 ie. --foo_bar is converted to --foo-bar. Non-option arguments
38 are left untouched.
39 """
40+
41+ @staticmethod
42+ def get_prog():
43+ prog = "mojo"
44+ try:
45+ prog_name = os.path.basename(sys.argv[0])
46+ if prog_name in ("__main__.py", "-c"):
47+ prog = "{0} -m mojo".format(sys.executable)
48+ else:
49+ prog = prog_name
50+ except (AttributeError, TypeError, IndexError):
51+ pass
52+ return prog
53+
54+ def __init__(self, *args, **kwargs):
55+ if "prog" not in kwargs or kwargs["prog"] is None:
56+ kwargs["prog"] = self.get_prog()
57+ super(MojoArgumentParser, self).__init__(*args, **kwargs)
58+
59 def add_argument(self, *args, **kwargs):
60 # Replace _ with - to create a consistent cli, no matter
61 # what the callsite chose. We don't need to worry about
62@@ -384,22 +403,9 @@
63 namespace)
64
65
66-def get_prog():
67- prog = "mojo"
68- try:
69- prog_name = os.path.basename(sys.argv[0])
70- if prog_name in ("__main__.py", "-c"):
71- prog = "{0} -m mojo".format(sys.executable)
72- else:
73- prog = prog_name
74- except (AttributeError, TypeError, IndexError):
75- pass
76- return prog
77-
78-
79 def create_arg_parser():
80 # main program parser
81- ap = MojoArgumentParser(prog=get_prog())
82+ ap = MojoArgumentParser()
83 ap.add_argument("--blunt", action="store_true",
84 help="Handle fewer exceptions")
85 ap.add_argument("--break_everything", action="store_true",
86@@ -408,13 +414,12 @@
87 help="Log level. Checks $MOJO_LOGLEVEL, defaults to INFO")
88 ap.add_argument("--logfile", "-L", default=os.environ.get('MOJO_LOGFILE'),
89 help="File to log to in addition to stdout. Overrides environment "
90- "variable MOJO_LOGFILE. Defaults to MOJO_WORKSPACE/log/mojo.log")
91+ "variable MOJO_LOGFILE. Defaults to MOJO_WORKSPACE/log/mojo.log")
92 ap.add_argument("-r", "--mojo_root", default=os.environ.get('MOJO_ROOT'),
93 help="Root directory that mojo works out of Overrides the MOJO_ROOT environment variable. "
94- "Defaults to /srv/mojo for projects using 'lxc' containers and to "
95- " ~/.local/share/mojo for 'lxd' and 'containerless' projects.")
96- ap.add_argument('--version', action='version',
97- version=mojo.__version__)
98+ "Defaults to /srv/mojo for projects using 'lxc' containers and to "
99+ " ~/.local/share/mojo for 'lxd' and 'containerless' projects.")
100+ ap.add_argument('--version', action='version', version=mojo.__version__)
101
102 # subcommand parser
103 sp = ap.add_subparsers(title="subcommands", dest="subparser_name")
104@@ -424,7 +429,7 @@
105 project_kwargs = {'help': "Project name. Overrides the MOJO_PROJECT environment variable."}
106 series_args = ("-s", "--series")
107 series_kwargs = {'default': os.environ.get('MOJO_SERIES'),
108- 'help': "Distro series. Overrides the MOJO_SERIES environment variable."}
109+ 'help': "Distro series. Overrides the MOJO_SERIES environment variable."}
110 stage_args = ("--stage", )
111 stage_kwargs = {'default': os.environ.get('MOJO_STAGE', 'devel'),
112 'help': "Deployment stage. Overrides MOJO_STAGE environment variable (default: devel)"}
113@@ -444,32 +449,32 @@
114 phaseopts.add_argument(*series_args, **series_kwargs)
115 phaseopts.add_argument("--spec-url", default=None,
116 help='Location of the Mojo spec. Overrides the '
117- 'MOJO_SPEC environment variable. If MOJO_SPEC '
118- 'is unset, defaults to ".".')
119+ 'MOJO_SPEC environment variable. If MOJO_SPEC '
120+ 'is unset, defaults to ".".')
121
122 # project-new subcommand - creates a new mojo project
123 prjnewcmd = sp.add_parser("project-new",
124- description="Create a new Mojo project and container")
125+ description="Create a new Mojo project and container")
126 prjnewcmd.add_argument(*series_args, required=False, **series_kwargs)
127 prjnewcmd.add_argument("name", nargs='?', default=os.environ.get('MOJO_PROJECT'),
128- help="Base name of the project ([a-zA-Z0-9._]*)")
129+ help="Base name of the project ([a-zA-Z0-9._]*)")
130 prjnewcmd.add_argument('-c', "--container", default=os.environ.get('MOJO_CONTAINER', 'lxc'),
131- choices=['lxc', 'lxd', 'containerless'],
132- help="Container. Options 'lxc', 'lxd' or 'containerless'. "
133- "Defaults to 'lxc'")
134+ choices=['lxc', 'lxd', 'containerless'],
135+ help="Container. Options 'lxc', 'lxd' or 'containerless'. "
136+ "Defaults to 'lxc'")
137 prjnewcmd.set_defaults(func=call_project_new)
138
139 # project-destroy subcommand - deletes a mojo project
140 prjdelcmd = sp.add_parser("project-destroy",
141 description="Destroy a Mojo project and container")
142 prjdelcmd.add_argument("name", default=os.environ.get('MOJO_PROJECT'),
143- help="Base name of the project ([a-zA-Z0-9._]*)")
144+ help="Base name of the project ([a-zA-Z0-9._]*)")
145 prjdelcmd.add_argument(*series_args, **series_kwargs)
146 prjdelcmd.set_defaults(func=call_project_destroy)
147
148 # project-list
149 prjlistcmd = sp.add_parser("project-list",
150- description="List existing projects, found at the mojo root")
151+ description="List existing projects, found at the mojo root")
152 prjlistcmd.add_argument("-n", "--names", action="store_true",
153 help="List only project names, without series")
154 prjlistcmd.set_defaults(func=list_projects)
155@@ -477,11 +482,11 @@
156 # workspace-new subcommand - creates a new workspace for a specific spec
157 # revision
158 wsnewcmd = sp.add_parser("workspace-new",
159- description="Initialize a mojo workspace")
160+ description="Initialize a mojo workspace")
161 wsnewcmd.add_argument("spec_url", nargs='?', default=None,
162 help='Location of the Mojo spec. Overrides the '
163- 'MOJO_SPEC environment variable. If MOJO_SPEC '
164- 'is unset, defaults to ".".')
165+ 'MOJO_SPEC environment variable. If MOJO_SPEC '
166+ 'is unset, defaults to ".".')
167 wsnewcmd.add_argument("workspace", nargs='?', default=None, help=workspace_kwargs['help'])
168 wsnewcmd.add_argument(*project_args, **project_kwargs)
169 wsnewcmd.add_argument(*series_args, **series_kwargs)
170@@ -490,7 +495,7 @@
171
172 # workspace-destroy subcommand - deletes a mojo project
173 wsdelcmd = sp.add_parser("workspace-destroy",
174- description="Destroy a mojo workspace")
175+ description="Destroy a mojo workspace")
176 wsdelcmd.add_argument(*project_args, **project_kwargs)
177 wsdelcmd.add_argument(*series_args, **series_kwargs)
178 wsdelcmd.add_argument(*workspace_args, **workspace_kwargs)
179@@ -498,7 +503,7 @@
180
181 # workspace-list subcommand
182 wslistcmd = sp.add_parser("workspace-list",
183- description="List workspaces for a project")
184+ description="List workspaces for a project")
185 wslistcmd.add_argument(*project_args, **project_kwargs)
186 wslistcmd.add_argument(*series_args, **series_kwargs)
187 wslistcmd.set_defaults(func=list_workspaces)
188@@ -516,14 +521,14 @@
189
190 # charm-audit subcommand - runs a charm audit
191 charmauditcmd = sp.add_parser("charm-audit",
192- description="Run a charm audit",
193- parents=[phaseopts])
194+ description="Run a charm audit",
195+ parents=[phaseopts])
196 charmauditcmd.set_defaults(func=run_phase)
197
198 # build subcommand - run project build script inside container
199 buildcmd = sp.add_parser("build",
200- description="Execute project build script",
201- parents=[phaseopts])
202+ description="Execute project build script",
203+ parents=[phaseopts])
204 buildcmd.set_defaults(func=run_phase)
205
206 # repo subcommand - assembles charms into a repository
207@@ -533,50 +538,50 @@
208
209 # sleep subcommand - sleeps for n seconds
210 sleepcmd = sp.add_parser("sleep", description="Sleep for time in seconds",
211- parents=[phaseopts])
212+ parents=[phaseopts])
213 sleepcmd.set_defaults(func=run_phase)
214
215 # secrets subcommand - secretss for n seconds
216 secretscmd = sp.add_parser("secrets",
217- description="Copy secrets from staged "
218- "location to workspace local",
219- parents=[phaseopts])
220+ description="Copy secrets from staged "
221+ "location to workspace local",
222+ parents=[phaseopts])
223 secretscmd.set_defaults(func=run_phase)
224
225 # script subcommand - run an arbitrary script inside container
226 scriptcmd = sp.add_parser("script", description="Execute a project script",
227- parents=[phaseopts])
228+ parents=[phaseopts])
229 scriptcmd.set_defaults(func=run_phase)
230
231 # bundle subcommand - run a juju deploy using bundles
232 bundlecmd = sp.add_parser("bundle",
233- description="Deploy applications and relations using juju bundles",
234- parents=[phaseopts])
235+ description="Deploy applications and relations using juju bundles",
236+ parents=[phaseopts])
237 bundlecmd.set_defaults(func=run_phase)
238
239 # deploy subcommand - run a juju-deployer session
240 deploycmd = sp.add_parser("deploy",
241- description="Deploy services and/or create "
242- "relations",
243- parents=[phaseopts])
244+ description="Deploy services and/or create "
245+ "relations",
246+ parents=[phaseopts])
247 deploycmd.add_argument("--ignore", action="store_true", default=False,
248 help="Exit 0 even if service deployment fails")
249 deploycmd.set_defaults(func=run_phase)
250
251 # deploy-show subcommand
252 deployshowcmd = sp.add_parser("deploy-show",
253- description="Show rendered juju-deployer "
254- "configuration",
255- parents=[phaseopts])
256+ description="Show rendered juju-deployer "
257+ "configuration",
258+ parents=[phaseopts])
259 deployshowcmd.set_defaults(func=show_deploy)
260
261 # deploy-diff subcommand
262 deploydiffcmd = sp.add_parser("deploy-diff",
263- description="Show juju-deployer diff. "
264- "Generate a delta between a "
265- "configured deployment and a "
266- "running environment.",
267- parents=[phaseopts])
268+ description="Show juju-deployer diff. "
269+ "Generate a delta between a "
270+ "configured deployment and a "
271+ "running environment.",
272+ parents=[phaseopts])
273
274 configapplicationgroup = deploydiffcmd.add_mutually_exclusive_group()
275 configapplicationgroup.add_argument(
276@@ -592,37 +597,37 @@
277
278 # volumes subcommand - mount volumes
279 volcmd = sp.add_parser("volumes", description="Mount volumes",
280- parents=[phaseopts])
281+ parents=[phaseopts])
282 volcmd.set_defaults(func=run_phase)
283
284 # verify subcommand - verify that a deployment was successful
285 verifycmd = sp.add_parser("verify",
286- description="Verify that the deployment is "
287- "working",
288- parents=[phaseopts])
289+ description="Verify that the deployment is "
290+ "working",
291+ parents=[phaseopts])
292 verifycmd.set_defaults(func=run_phase)
293
294 # juju-check-wait subcommand - wait until juju environment reaches steady state
295 jujucheckwaitcmd = sp.add_parser("juju-check-wait",
296- description="Wait until juju environment "
297- "reaches steady state.",
298- parents=[phaseopts])
299+ description="Wait until juju environment "
300+ "reaches steady state.",
301+ parents=[phaseopts])
302 jujucheckwaitcmd.set_defaults(func=run_phase)
303
304 # nagios-check subcommand - run all nagios checks in an environment
305 nagioscheckcmd = sp.add_parser("nagios-check",
306- description="Run all nagios checks in the environment",
307- parents=[phaseopts])
308+ description="Run all nagios checks in the environment",
309+ parents=[phaseopts])
310 nagioscheckcmd.set_defaults(func=run_phase)
311
312 # run subcommand - run an entire spec per its manifest
313 runcmd = sp.add_parser("run",
314- description="Run an entire deployment from start "
315- "to finish")
316+ description="Run an entire deployment from start "
317+ "to finish")
318 runcmd.add_argument("spec_url", nargs='?', default=None,
319 help='Location of the Mojo spec. Overrides the '
320- 'MOJO_SPEC environment variable. If MOJO_SPEC '
321- 'is unset, defaults to ".".')
322+ 'MOJO_SPEC environment variable. If MOJO_SPEC '
323+ 'is unset, defaults to ".".')
324 runcmd.add_argument("workspace", nargs='?', default=None, help=workspace_kwargs['help'])
325 runcmd.add_argument("-i", "--interactive", action="store_true", default=False,
326 help="Run the manifest in interactive mode, prompting for each phase before running it")
327@@ -641,7 +646,7 @@
328 # help subcommand - get help for other subcommands
329 helpcmd = sp.add_parser("help", description="Interactive help")
330 helpcmd.add_argument("command", nargs='?', default=None,
331- help="Get help for this sub-command.")
332+ help="Get help for this sub-command.")
333 helpcmd.set_defaults(func=display_help, parser=ap)
334
335 return ap
336@@ -654,6 +659,9 @@
337 mojo.CacheDisk.ttl(cache_ttl)
338 try:
339 args.func(args)
340+ # XXX: Handle attribute errors when calling bare 'mojo' command on
341+ # XXX: python 3 -- this prints usage and exits with an exit code 1
342+ # XXX: on python 2
343 except AttributeError as e:
344 if args.subparser_name is None and ap is not None:
345 ap.print_usage()
346
347=== modified file 'mojo/contain.py'
348--- mojo/contain.py 2020-02-06 16:39:01 +0000
349+++ mojo/contain.py 2020-02-20 21:22:11 +0000
350@@ -8,7 +8,6 @@
351
352 from __future__ import absolute_import, division, print_function
353 import ast
354-import distutils.spawn
355 import json
356 import logging
357 import os
358
359=== modified file 'mojo/juju/__init__.py'
360--- mojo/juju/__init__.py 2020-02-03 23:44:35 +0000
361+++ mojo/juju/__init__.py 2020-02-20 21:22:11 +0000
362@@ -1,4 +1,4 @@
363-from __future__ import absolute_import, division, print_function
364+from __future__ import division, print_function
365 import subprocess
366
367 from .debuglogs import DebugLogs
368
369=== modified file 'mojo/juju/check.py'
370--- mojo/juju/check.py 2020-02-03 23:44:35 +0000
371+++ mojo/juju/check.py 2020-02-20 21:22:11 +0000
372@@ -2,7 +2,7 @@
373 The output and exit code matches that expected by Nagios for Okay (0) or Critical (2).
374 Juju version 2 only.
375 """
376-from __future__ import absolute_import, division, print_function
377+from __future__ import division, print_function
378 import argparse
379 import inspect
380 import sys
381
382=== modified file 'mojo/juju/checks.py'
383--- mojo/juju/checks.py 2020-02-03 23:44:35 +0000
384+++ mojo/juju/checks.py 2020-02-20 21:22:11 +0000
385@@ -1,4 +1,4 @@
386-from __future__ import absolute_import, division, print_function
387+from __future__ import division, print_function
388
389 class JujuCheckError(Exception):
390 pass
391
392=== modified file 'mojo/juju/debuglogs.py'
393--- mojo/juju/debuglogs.py 2020-02-03 23:44:35 +0000
394+++ mojo/juju/debuglogs.py 2020-02-20 21:22:11 +0000
395@@ -1,13 +1,13 @@
396-from __future__ import absolute_import, division, print_function
397+from __future__ import division, print_function
398 import ast
399 import logging
400 import subprocess
401 import yaml
402
403 from .status import Status
404-from .utils import get_command_path
405
406 from ..exceptions import ConfigNotFoundException
407+from ..shutil_which import get_command
408
409 DEFAULT_DEBUG_LOG_CONFIG = {}
410
411@@ -22,7 +22,7 @@
412 self.debug_logs_stages_to_exclude = debug_logs_stages_to_exclude or 'production'
413 self.stage = stage
414 self.stage_name = stage.split('/')[-1]
415- self.exec_path = get_command_path("juju")
416+ self.exec_path, _ = get_command("juju")
417 self.workspace = workspace
418 self.supported_types = {
419 'config-files': {
420
421=== modified file 'mojo/juju/parse_status.py'
422--- mojo/juju/parse_status.py 2020-02-03 23:44:35 +0000
423+++ mojo/juju/parse_status.py 2020-02-20 21:22:11 +0000
424@@ -1,7 +1,7 @@
425 """ Provides convenience commands for extracting information from Juju status
426 Juju version 2 only.
427 """
428-from __future__ import absolute_import, division, print_function
429+from __future__ import division, print_function
430 import argparse
431 import sys
432
433
434=== modified file 'mojo/juju/status.py'
435--- mojo/juju/status.py 2020-02-03 23:44:35 +0000
436+++ mojo/juju/status.py 2020-02-20 21:22:11 +0000
437@@ -1,6 +1,5 @@
438-from __future__ import absolute_import, division, print_function
439+from __future__ import division, print_function
440 import datetime
441-import distutils.spawn
442 import logging
443 import os
444 import subprocess
445@@ -10,11 +9,10 @@
446 from collections import Counter
447
448 from .wait import wait
449+from ..shutil_which import get_command
450
451 # We can't use the implementation in .utils due to a circular import
452-juju_path = distutils.spawn.find_executable("juju")
453-if not juju_path:
454- juju_path = "juju"
455+juju_path, _ = get_command("juju")
456
457 try:
458 with open(os.devnull, 'w') as devnull:
459@@ -219,7 +217,7 @@
460 [juju_path, 'status', '--format=tabular'], universal_newlines=True))
461 raise JujuStatusError("Timed out checking Juju status for stable state")
462 stable_state = []
463- for _, check_info in checks.items():
464+ for check_info in checks.values():
465 for check in check_info:
466 check_function = check['check_func']
467 states = check_function()
468@@ -489,14 +487,11 @@
469 Returns: The controller version reported by Juju
470 """
471 controller_yaml, controllers = None, {}
472- try:
473- controller_yaml = subprocess.check_output([juju_path, 'controllers', '--format', 'yaml'], universal_newlines=True)
474- except subprocess.CalledProcessError:
475- logging.exception("Failed to load controller data")
476- else:
477- controllers = yaml.safe_load(controller_yaml)
478- current_controller = controllers.get('current-controller')
479- return controllers.get('controllers', {}).get(current_controller, {}).get('agent-version')
480+ controllers = yaml.safe_load(
481+ subprocess.check_output([juju_path, 'controllers', '--format', 'yaml'], universal_newlines=True)
482+ )
483+ current_controller = controllers['current-controller']
484+ return controllers['controllers'][current_controller]['agent-version']
485
486 def machine_instance_id(self, machine_num):
487 machine = self.yaml_status()['machines'].get(machine_num)
488
489=== modified file 'mojo/juju/utils.py'
490--- mojo/juju/utils.py 2020-02-03 23:44:35 +0000
491+++ mojo/juju/utils.py 2020-02-20 21:22:11 +0000
492@@ -1,6 +1,6 @@
493 from __future__ import absolute_import, division, print_function
494-import distutils.spawn
495 from .status import major_version
496+from ..shutil_which import get_command
497
498
499 def get_juju_command(command):
500@@ -15,10 +15,3 @@
501 },
502 }
503 return juju_commands[major_version][command]
504-
505-
506-def get_command_path(command):
507- full_path = distutils.spawn.find_executable(command)
508- if full_path:
509- return full_path
510- return command
511\ No newline at end of file
512
513=== modified file 'mojo/juju/wait.py'
514--- mojo/juju/wait.py 2020-02-03 23:44:35 +0000
515+++ mojo/juju/wait.py 2020-02-20 21:22:11 +0000
516@@ -2,6 +2,9 @@
517 # This file is mirrored from lp:juju-wait, a juju plugin to wait for
518 # environment steady state.
519 #
520+# This file is part of juju-wait, a juju plugin to wait for environment
521+# steady state.
522+#
523 # Copyright 2015 Canonical Ltd.
524 #
525 # This program is free software: you can redistribute it and/or modify
526
527=== modified file 'mojo/phase.py'
528--- mojo/phase.py 2020-02-10 06:18:46 +0000
529+++ mojo/phase.py 2020-02-20 21:22:11 +0000
530@@ -21,12 +21,13 @@
531 get_dep_prog,
532 secs2human,
533 subprocess_run,
534- get_command,
535 )
536+from .juju.utils import get_juju_command
537 from jinja2 import Template
538 from jinja2.exceptions import TemplateSyntaxError
539 import codetree
540 from deployer.config import ConfigStack
541+from .shutil_which import get_command
542
543 JUJU_EXE_PATH, _ = get_command("juju")
544
545@@ -507,7 +508,7 @@
546 # Something differs, e.g. constraints, but not the config.
547 continue
548
549- juju_set_command = [JUJU_EXE_PATH, juju.utils.get_juju_command('set'), service]
550+ juju_set_command = [JUJU_EXE_PATH, get_juju_command('set'), service]
551 config_changes = []
552
553 for config_item, config_value in deployer_config_items.items():
554@@ -611,11 +612,7 @@
555 if os.path.isfile(config):
556 cmd.extend(['-c', config])
557 cmd.extend(["-l"])
558- try:
559- target = next(iter(subprocess.check_output(cmd, universal_newlines=True).split("\n")))
560- except subprocess.CalledProcessError:
561- logging.exception("Failed to find juju-deployer configuration target")
562- target = None
563+ target = subprocess.check_output(cmd, universal_newlines=True).split("\n")[0]
564 return target
565
566 def _validate_configs(self, configs):
567
568=== modified file 'mojo/project.py'
569--- mojo/project.py 2020-02-03 23:44:35 +0000
570+++ mojo/project.py 2020-02-20 21:22:11 +0000
571@@ -175,6 +175,9 @@
572 )
573 if mojo_root:
574 # resolve and normalize paths according to the same process for consistency
575+ # without this change, if a root is created with an absolute path by the user
576+ # or a project is created with an absolute path, the project will appear
577+ # twice as our verson of MOJO_ROOT is unexpanded.
578 mojo_roots.add(os.path.abspath(os.path.expanduser(mojo_root)))
579 proj_list = []
580 for mojo_root in mojo_roots:
581
582=== added file 'mojo/shutil_which.py'
583--- mojo/shutil_which.py 1970-01-01 00:00:00 +0000
584+++ mojo/shutil_which.py 2020-02-20 21:22:11 +0000
585@@ -0,0 +1,140 @@
586+# -*- coding=utf-8 -*-
587+from __future__ import division, print_function
588+
589+try:
590+ from shutil import which
591+except ImportError:
592+ import os
593+ import sys
594+
595+
596+ def fspath(path):
597+ """Return the path representation of a path-like object.
598+ If str or bytes is passed in, it is returned unchanged. Otherwise the
599+ os.PathLike interface is used to get the path representation. If the
600+ path representation is not str or bytes, TypeError is raised. If the
601+ provided path is not str, bytes, or os.PathLike, TypeError is raised.
602+ """
603+ if isinstance(path, (str, bytes)):
604+ return path
605+
606+ # Work from the object's type to match method resolution of other magic
607+ # methods.
608+ path_type = type(path)
609+ try:
610+ path_repr = path_type.__fspath__(path)
611+ except AttributeError:
612+ if hasattr(path_type, '__fspath__'):
613+ raise
614+ else:
615+ raise TypeError("expected str, bytes or os.PathLike object, "
616+ "not " + path_type.__name__)
617+ if isinstance(path_repr, (str, bytes)):
618+ return path_repr
619+ else:
620+ raise TypeError("expected {}.__fspath__() to return str or bytes, "
621+ "not {}".format(path_type.__name__,
622+ type(path_repr).__name__))
623+
624+
625+ def _fscodec():
626+ encoding = sys.getfilesystemencoding()
627+ error_func = getattr(sys, "getfilesystemencodeerrors", lambda: "surrogateescape")
628+ errors = error_func()
629+ def _fsencode(path):
630+ """
631+ backport of `fsencode` for shutil
632+ """
633+ filename = fspath(filename) # Does type-checking of `filename`.
634+ if isinstance(filename, str):
635+ return filename.encode(encoding, errors)
636+ else:
637+ return filename
638+
639+ def _fsdecode(path):
640+ filename = fspath(filename) # Does type-checking of `filename`.
641+ if isinstance(filename, bytes):
642+ return filename.decode(encoding, errors)
643+ else:
644+ return filename
645+
646+ return _fsencode, _fsdecode
647+
648+
649+ fsencode, fsdecode = _fscodec()
650+ del _fscodec
651+
652+
653+ # Check that a given file can be accessed with the correct mode.
654+ # Additionally check that `file` is not a directory, as on Windows
655+ # directories pass the os.access check.
656+ def _access_check(fn, mode):
657+ return (os.path.exists(fn) and os.access(fn, mode)
658+ and not os.path.isdir(fn))
659+
660+
661+ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
662+ """Given a command, mode, and a PATH string, return the path which
663+ conforms to the given mode on the PATH, or None if there is no such
664+ file.
665+ `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
666+ of os.environ.get("PATH"), or can be overridden with a custom search
667+ path.
668+ """
669+ # If we're given a path with a directory part, look it up directly rather
670+ # than referring to PATH directories. This includes checking relative to the
671+ # current directory, e.g. ./script
672+ if os.path.dirname(cmd):
673+ if _access_check(cmd, mode):
674+ return cmd
675+ return None
676+
677+ use_bytes = isinstance(cmd, bytes)
678+
679+ if path is None:
680+ path = os.environ.get("PATH", None)
681+ if path is None:
682+ try:
683+ path = os.confstr("CS_PATH")
684+ except (AttributeError, ValueError):
685+ # os.confstr() or CS_PATH is not available
686+ path = os.defpath
687+ # bpo-35755: Don't use os.defpath if the PATH environment variable is
688+ # set to an empty string
689+
690+ # PATH='' doesn't match, whereas PATH=':' looks in the current directory
691+ if not path:
692+ return None
693+
694+ if use_bytes:
695+ path = os.fsencode(path)
696+ path = path.split(os.fsencode(os.pathsep))
697+ else:
698+ path = os.fsdecode(path)
699+ path = path.split(os.pathsep)
700+ files = [cmd]
701+ seen = set()
702+ for dir in path:
703+ normdir = os.path.normcase(dir)
704+ if not normdir in seen:
705+ seen.add(normdir)
706+ for thefile in files:
707+ name = os.path.join(dir, thefile)
708+ if _access_check(name, mode):
709+ return name
710+ return None
711+
712+
713+def get_command(cmd):
714+ """
715+ Given a command, look up that command and determine its absolute path
716+
717+ :param str cmd: The command to look up
718+ :return: A 2-tuple of the resulting command and a success/failure flag,
719+ True indicating success, False indicating failure
720+ :rtype: Tuple[str, bool]
721+ """
722+ full_cmd = which(cmd)
723+ if not full_cmd:
724+ return cmd, False
725+ return full_cmd, True
726\ No newline at end of file
727
728=== modified file 'mojo/tests/test_cli.py'
729--- mojo/tests/test_cli.py 2020-01-22 01:16:37 +0000
730+++ mojo/tests/test_cli.py 2020-02-20 21:22:11 +0000
731@@ -20,7 +20,7 @@
732 MojoArgumentParser,
733 )
734 from mojo.exceptions import ArgumentError
735-from mojo.utils import PREFERRED_ENCODING
736+from mojo.utils import getpreferredencoding
737 import mojo.project
738 from mojo.tests.utils import create_unique_bzr_branch
739
740@@ -58,7 +58,7 @@
741 p.print_help(h)
742
743 # An underscore in options is converted to a hyphen.
744- self.assertTrue('--ying-yang' in six.ensure_text(h.getvalue(), PREFERRED_ENCODING) )
745+ self.assertTrue('--ying-yang' in six.ensure_text(h.getvalue(), getpreferredencoding()) )
746 self.assertEqual(p.parse_args(['-a', 'val']).foo_bar, 'val')
747 self.assertEqual(p.parse_args(['--foo-bar', 'val']).foo_bar, 'val')
748 self.assertEqual(p.parse_args(['--ying-yang', 'val']).foo_bar, 'val')
749
750=== modified file 'mojo/tests/test_debuglogs.py'
751--- mojo/tests/test_debuglogs.py 2020-01-29 17:15:43 +0000
752+++ mojo/tests/test_debuglogs.py 2020-02-20 21:22:11 +0000
753@@ -6,6 +6,7 @@
754
755 from mojo.juju.debuglogs import DebugLogs, UnsupportedDebugLogType, DEFAULT_DEBUG_LOG_CONFIG
756 from mojo.tests import utils
757+from mojo.shutil_which import get_command
758 import six
759
760 if six.PY2:
761@@ -14,7 +15,7 @@
762 from unittest import mock
763
764
765-JUJU_PATH = utils.find_exe('juju')
766+JUJU_PATH, _ = get_command('juju')
767
768
769 class JujuDebugLogsTestCase(TestCase, utils.MojoTestCaseMixin):
770
771=== modified file 'mojo/tests/test_juju1.py'
772--- mojo/tests/test_juju1.py 2020-02-10 06:19:24 +0000
773+++ mojo/tests/test_juju1.py 2020-02-20 21:22:11 +0000
774@@ -6,6 +6,7 @@
775 import six
776 import mojo.juju
777 from mojo.tests import utils
778+from mojo.shutil_which import get_command
779
780 if six.PY2:
781 import mock
782@@ -13,6 +14,9 @@
783 from unittest import mock
784
785
786+JUJU_PATH, _ = get_command('juju')
787+
788+
789 NO_ERROR_STATUS = """environment: openstack
790 machines:
791 "0":
792@@ -215,13 +219,13 @@
793 no_env_status._get_status()
794 mock_check_output.assert_called_with(
795 ['/usr/bin/timeout', '--kill-after', '5', '600',
796- '/snap/bin/juju', 'status', '--format=yaml'], universal_newlines=True)
797+ JUJU_PATH, 'status', '--format=yaml'], universal_newlines=True)
798 # Second test with an environment set
799 env_status = mojo.juju.status.Juju1Status(environment="test-env")
800 env_status._get_status()
801 mock_check_output.assert_called_with(
802 ['/usr/bin/timeout', '--kill-after', '5', '600',
803- '/snap/bin/juju', 'status', '--format=yaml', '-e', 'test-env'], universal_newlines=True)
804+ JUJU_PATH, 'status', '--format=yaml', '-e', 'test-env'], universal_newlines=True)
805
806 with mock.patch('subprocess.check_output') as mock_check_output:
807 # Check that if we're calling _get_status where _status_output is
808@@ -234,7 +238,7 @@
809 self.status._get_status(force_update=True)
810 mock_check_output.assert_called_with(
811 ['/usr/bin/timeout', '--kill-after', '5', '600',
812- '/snap/bin/juju', 'status', '--format=yaml'], universal_newlines=True)
813+ JUJU_PATH, 'status', '--format=yaml'], universal_newlines=True)
814
815 def test_yaml_status(self):
816 """Test the yaml_status function"""
817
818=== modified file 'mojo/tests/test_juju2.py'
819--- mojo/tests/test_juju2.py 2020-02-10 06:19:24 +0000
820+++ mojo/tests/test_juju2.py 2020-02-20 21:22:11 +0000
821@@ -10,7 +10,7 @@
822 import mojo.juju
823 from mojo.juju.check import main as check
824 from mojo.juju.parse_status import main as parse_status
825-from .utils import find_exe
826+from mojo.shutil_which import get_command
827
828 if six.PY2:
829 import mock
830@@ -18,7 +18,7 @@
831 from unittest import mock
832
833
834-JUJU_PATH = find_exe('juju')
835+JUJU_PATH, _ = get_command('juju')
836
837
838 class Juju2StatusTestCase(TestCase):
839@@ -183,7 +183,7 @@
840 env_status.status()
841 mock_check_output.assert_called_with(
842 ['/usr/bin/timeout', '--kill-after', '5', '600',
843- '/snap/bin/juju', 'status', '--format=yaml', '-m', 'test-env'], universal_newlines=True)
844+ JUJU_PATH, 'status', '--format=yaml', '-m', 'test-env'], universal_newlines=True)
845
846 with mock.patch('subprocess.check_output') as mock_check_output:
847 # Check that if we're calling status() where _raw_status is
848@@ -196,7 +196,7 @@
849 self.status.status(force_update=True)
850 mock_check_output.assert_called_with(
851 ['/usr/bin/timeout', '--kill-after', '5', '600',
852- '/snap/bin/juju', 'status', '--format=yaml'], universal_newlines=True)
853+ JUJU_PATH, 'status', '--format=yaml'], universal_newlines=True)
854
855 @mock.patch('mojo.juju.status.check_output_with_timeout')
856 def test_yaml_status(self, _check_output_with_timeout):
857
858=== modified file 'mojo/tests/test_phase.py'
859--- mojo/tests/test_phase.py 2020-02-10 06:19:24 +0000
860+++ mojo/tests/test_phase.py 2020-02-20 21:22:11 +0000
861@@ -26,6 +26,7 @@
862 )
863 from mojo.tests import utils
864 from mojo.project import Project
865+from mojo.shutil_which import get_command
866
867 if six.PY2:
868 import mock
869@@ -33,7 +34,7 @@
870 from unittest import mock
871
872
873-JUJU_PATH = utils.find_exe('juju')
874+JUJU_PATH, _ = get_command('juju')
875
876
877 class DeployerPhaseTestCase(TestCase, utils.MojoTestCaseMixin):
878
879=== modified file 'mojo/tests/utils.py'
880--- mojo/tests/utils.py 2020-01-29 17:15:43 +0000
881+++ mojo/tests/utils.py 2020-02-20 21:22:11 +0000
882@@ -89,11 +89,4 @@
883 # From http://stackoverflow.com/a/1160227/523729
884 def touch(filename, times=None):
885 with open(filename, 'a'):
886- os.utime(filename, times)
887-
888-
889-def find_exe(name):
890- path = distutils.spawn.find_executable(name)
891- if path is not None:
892- return path
893- return name
894\ No newline at end of file
895+ os.utime(filename, times)
896\ No newline at end of file
897
898=== modified file 'mojo/utils.py'
899--- mojo/utils.py 2020-02-10 06:17:54 +0000
900+++ mojo/utils.py 2020-02-20 21:22:11 +0000
901@@ -2,9 +2,9 @@
902 # GNU General Public License version 3 (see the file LICENSE).
903 from __future__ import absolute_import, division, print_function
904
905-import distutils.spawn
906 import errno
907 import grp
908+import io
909 import locale
910 import logging
911 import os
912@@ -22,6 +22,7 @@
913 from six.moves.cPickle import dump as pickle_dump, load as pickle_load
914
915 from .exceptions import Unprivileged
916+from .shutil_which import get_command
917
918
919 class CacheDisk():
920@@ -248,22 +249,28 @@
921 return None
922
923
924-def get_command(cmd):
925- """
926- Given a command, look up that command and determine its absolute path
927-
928- :param str cmd: The command to look up
929- :return: A 2-tuple of the resulting command and a success/failure flag,
930- True indicating success, False indicating failure
931- :rtype: Tuple[str, bool]
932- """
933- full_cmd = distutils.spawn.find_executable(cmd)
934- if not full_cmd:
935- return cmd, False
936- return full_cmd, True
937-
938-
939 def subprocess_run(cmd, include_env=True, shell=False, check_output=False, env=None, **kwargs):
940+ """
941+ Run a command in a subprocess
942+
943+ Given an arbitrary command, resolve the command to its executable and run
944+ it in a subprocess. If *cmd* is a :class:`str` and *shell* is **False**,
945+ *cmd* will be split using :func:`shlex.split` and resolved. If the first
946+ command discovered is ``sudo``, the command supplied to ``sudo`` will also
947+ be resolved.
948+
949+ :param Union[str, list, tuple] cmd: A command to execute in a subprocess
950+ :param bool include_env: Whether to include the environment, defaults to True
951+ :param bool shell: Pass ``shell=True`` to the subprocess command, defaults
952+ to False
953+ :param bool check_output: Use ``check_output`` instead of ``check_call``,
954+ defaults to False
955+ :param Dict[str, str] env: Extra environment variables to set in the
956+ subprocess, defaults to None
957+ :return: The standard output of the subprocess if *check_call* is True,
958+ otherwise nothing
959+ :rtype: Union[str, None]
960+ """
961 if not isinstance(cmd, (list, tuple)) and not shell:
962 cmd = shlex.split(cmd)
963 if isinstance(cmd, (list, tuple)):
964@@ -308,7 +315,19 @@
965
966
967 def encode_for_stream(text, stream):
968- stream_encoding = getattr(stream, "encoding", PREFERRED_ENCODING)
969+ """
970+ Encode the given text for the given stream
971+
972+ On python 2, standard streams are binary streams, but python 3 streams
973+ are wrapped in :class:`~io.TextIOWrapper` instances and must be passed
974+ ``utf-8`` or traversed to find the wrapped buffer.
975+
976+ :param str text: A string or bytes-type to send to the given stream
977+ :param file stream: A file-like instance, usually a standard stream
978+ :return: The originally provided text re-encoded for the given stream
979+ :rtype: Union[str, bytes]
980+ """
981+ stream_encoding = getattr(stream, "encoding", getpreferredencoding())
982 if not _is_binary(stream):
983 return six.ensure_text(text, encoding=stream_encoding)
984 return six.ensure_binary(text, encoding=stream_encoding)
985@@ -353,7 +372,7 @@
986
987 status = pipe.wait()
988
989- return status, six.ensure_text(output, encoding=PREFERRED_ENCODING)
990+ return status, six.ensure_text(output, encoding=getpreferredencoding())
991
992
993 # Decorator for methods that must be run as root
994@@ -421,90 +440,29 @@
995 return output
996
997
998-def parse_release_info(filename):
999- """
1000- Parse a file with ``=`` delimited key-value pairs, returning a dictionary
1001-
1002- This function was originally designed for parsing system version info files
1003- but can be used for any file containing ``=`` delimited key value pair
1004-
1005- :param str filename: A file containing key value pairs, e.g.
1006- ``/etc/os-release``
1007- :return: A dictionary of parsed key value pairs from the provided file
1008- :rtype: Dict[str, str]
1009- """
1010- results = {}
1011- equivalent_keys = {"version_codename": "codename", "ubuntu_codename": "codename"}
1012- with open(filename, "r") as fh:
1013- lexer = shlex.shlex(fh)
1014- lexer.whitespace_split = True
1015- if six.PY2:
1016- lexer.wordchars = six.text_type(lexer.wordchars, encoding="iso-8859-1")
1017- for line in list(lexer):
1018- k, _, v = line.partition("=")
1019- if not (k and v):
1020- continue
1021- results[k.lower()] = six.ensure_text(v).strip('"')
1022- if "codename" not in results:
1023- equivalent_key = next(iter(k for k in equivalent_keys if k in results), None)
1024- if equivalent_key is not None:
1025- codename = results[equivalent_key]
1026- else:
1027- matches = re.search(r'(?:(?:\(|,\s*))(?P<codename>[\D]+[^\s\)])(?:\))?', results.get("version", ""))
1028- codename = matches.groupdict().get("codename", None) if matches else None
1029- if codename:
1030- results["codename"] = codename
1031- return results
1032-
1033-
1034-def get_release_dict(base_dir="/etc", use_os_release=True):
1035- """
1036- Build a release dict, looking in **base_dir** for release files to parse
1037-
1038- :param str base_dir: Base config directory to search, defaults to "/etc"
1039- :param bool use_os_release: Whether to include ``/etc/os-release`` when
1040- building the dictionary, defaults to True. Disable this to use the
1041- python2 style behavior.
1042- :return: A dictionary compiled from all matching release files found in
1043- the supplied directory
1044- :rtype: Dict[str, str]
1045- """
1046- release_re = r"(?P<name>\w+)[-_](release|version)"
1047- if six.PY2:
1048- _release_fn = re.compile(r"(?P<name>\w+)[-_](release|version)")
1049- else:
1050- _release_fn = re.compile(r"(?P<name>\w+)[-_](release|version)", re.ASCII)
1051- release_dict = {}
1052- # by default this file contains the majority of the info we want on unix
1053- # based systems. This should be disabled when needing python2 compatibility
1054- _base_release_file = "os-release"
1055- # Current implementations assume we should prioritize alphabetically,
1056- # so we will build one giant dictionary starting with a reverse-alphabet
1057- # based sorting
1058- for config_fn in sorted(os.listdir(base_dir), reverse=True):
1059- full_config_path = os.path.abspath(os.path.join(base_dir, config_fn))
1060- release_match = _release_fn.match(config_fn)
1061- base_release_match = config_fn == _base_release_file and use_os_release
1062- if not (release_match or base_release_match) or not os.path.isfile(full_config_path):
1063+def parse_os_version_file(path):
1064+ """
1065+ Since we are on ubuntu only we can use a simple implementation for this
1066+
1067+ :param str path: A file path to parse
1068+ :return: A 3-tuple of (name, version, id)
1069+ :rtype: Tuple[Optional[str], Optional[str], Optional[str]]
1070+ """
1071+ contents = []
1072+ name, version, id_ = None, None, None
1073+ with io.open(path, "r") as fh:
1074+ contents = fh.readlines()
1075+ for line in contents:
1076+ if "=" not in line:
1077 continue
1078- release_file_dict = parse_release_info(full_config_path)
1079- if not release_file_dict.get("name"):
1080- release_file_dict.update(release_match.groupdict())
1081- # If we are pulling the distro name from the file
1082- # then there's a good chance the file only has one line
1083- # which contains version info, e.g. "buster/sid".
1084- # Note that this is the closest way to replicate the python 2
1085- # behavior.
1086- if not release_file_dict.get("version"):
1087- version_data = None
1088- with open(full_config_path, "r") as fh:
1089- version_data = fh.read().strip()
1090- # if the file has more than one line of content I don't
1091- # really know what that would mean, so lets just ignore that
1092- if version_data and len(version_data.splitlines()) == 1:
1093- release_file_dict["version"] = version_data
1094- release_dict.update(release_file_dict)
1095- return release_dict
1096+ k, _, v = line.partition("=")
1097+ if k.lower() == "name" and name is None:
1098+ name = v.strip('"').strip()
1099+ elif k.lower() in ("id", "distrib_id") and id_ is None:
1100+ id_ = v.strip('"').strip()
1101+ elif k.lower() == "version" and version is None:
1102+ version = v.strip('"').strip()
1103+ return name, version, id_
1104
1105
1106 def dist(base_dir="/etc"):
1107@@ -515,12 +473,24 @@
1108 :return: a 3-tuple representing (distname, version, id)
1109 :rtype: Tuple[str, str, str]
1110 """
1111- release_dict = get_release_dict(base_dir=base_dir, use_os_release=False)
1112- name = release_dict.get("name")
1113- if not name:
1114- name_key = next(iter(k for k in release_dict.keys() if "name" in k.lower()))
1115- name = release_dict[name_key]
1116- return name, release_dict.get("version", ""), release_dict.get("id", "")
1117+ debian_version = os.path.join(base_dir, "debian_version")
1118+ lsb_release = os.path.join(base_dir, "lsb-release")
1119+ os_release = os.path.join(base_dir, "os-release")
1120+ name, version, id_ = None, None, None
1121+ if os.path.exists(debian_version):
1122+ name = "debian"
1123+ with io.open(debian_version, "r") as fh:
1124+ version = fh.read().strip()
1125+ for path in (os_release, lsb_release):
1126+ if os.path.exists(path):
1127+ parsed_name, parsed_version, parsed_id_ = parse_os_version_file(path)
1128+ if name is None and parsed_name:
1129+ name = parsed_name
1130+ if version is None and parsed_version:
1131+ version = parsed_version
1132+ if id_ is None and parsed_id_:
1133+ id_ = parsed_id_
1134+ return name, version, id_
1135
1136
1137 def getpreferredencoding():
1138@@ -530,6 +500,3 @@
1139 # (see https://github.com/pyinvoke/invoke/blob/93af29d/invoke/runners.py#L881)
1140 _encoding = locale.getpreferredencoding(False)
1141 return _encoding
1142-
1143-
1144-PREFERRED_ENCODING = getpreferredencoding()
1145
1146=== modified file 'setup.py'
1147--- setup.py 2020-02-03 23:44:35 +0000
1148+++ setup.py 2020-02-20 21:22:11 +0000
1149@@ -39,7 +39,8 @@
1150 "juju-deployer >= 0.6.4",
1151 "pylxd",
1152 "python-cinderclient >= 1.0.0",
1153- "python-codetree @ bzr+http://bazaar.launchpad.net/~codetree-maintainers/codetree#egg=python-codetree"
1154+ "python-codetree @ bzr+http://bazaar.launchpad.net/~codetree-maintainers/codetree#egg=python-codetree",
1155+ "six",
1156 ],
1157 extras_require={
1158 "tests": [

Subscribers

People subscribed via source and target branches

to all changes: