Skip to content

Commit 37ce79a

Browse files
authored
Merge pull request #1211 from minrk/cli-defaults
reconcile CLI/config priority
2 parents b4fee8e + 31e9722 commit 37ce79a

File tree

4 files changed

+130
-36
lines changed

4 files changed

+130
-36
lines changed

repo2docker/__main__.py

Lines changed: 77 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def get_argparser():
9393

9494
argparser.add_argument(
9595
"--json-logs",
96-
default=False,
96+
default=None,
9797
action="store_true",
9898
help="Emit JSON logs instead of human readable logs",
9999
)
@@ -108,12 +108,13 @@ def get_argparser():
108108

109109
argparser.add_argument(
110110
"--image-name",
111-
help=("Name of image to be built. If unspecified will be " "autogenerated"),
111+
help="Name of image to be built. If unspecified will be autogenerated",
112112
type=validate_image_name,
113113
)
114114

115115
argparser.add_argument(
116116
"--ref",
117+
default=None,
117118
help=(
118119
"Reference to build instead of default reference. For example"
119120
" branch name or commit for a Git repository."
@@ -126,10 +127,16 @@ def get_argparser():
126127
"--no-build",
127128
dest="build",
128129
action="store_false",
129-
help=(
130-
"Do not actually build the image. Useful in conjunction " "with --debug."
131-
),
130+
help="Do not actually build the image. Useful in conjunction with --debug.",
131+
)
132+
133+
argparser.add_argument(
134+
"--build",
135+
dest="build",
136+
action="store_true",
137+
help="Build the image (default)",
132138
)
139+
argparser.set_defaults(build=None)
133140

134141
argparser.add_argument(
135142
"--build-memory-limit",
@@ -149,6 +156,14 @@ def get_argparser():
149156
help="Do not run container after it has been built",
150157
)
151158

159+
argparser.add_argument(
160+
"--run",
161+
dest="run",
162+
action="store_true",
163+
help="Run container after it has been built (default).",
164+
)
165+
argparser.set_defaults(run=None)
166+
152167
argparser.add_argument(
153168
"--publish",
154169
"-p",
@@ -164,6 +179,7 @@ def get_argparser():
164179
"--publish-all",
165180
"-P",
166181
dest="all_ports",
182+
default=None,
167183
action="store_true",
168184
help="Publish all exposed ports to random host ports.",
169185
)
@@ -174,13 +190,27 @@ def get_argparser():
174190
action="store_false",
175191
help="Don't clean up remote checkouts after we are done",
176192
)
193+
argparser.add_argument(
194+
"--clean",
195+
dest="clean",
196+
action="store_true",
197+
help="Clean up remote checkouts after we are done (default).",
198+
)
199+
argparser.set_defaults(clean=None)
177200

178201
argparser.add_argument(
179202
"--push",
180203
dest="push",
181204
action="store_true",
182205
help="Push docker image to repository",
183206
)
207+
argparser.add_argument(
208+
"--no-push",
209+
dest="push",
210+
action="store_false",
211+
help="Don't push docker image to repository (default).",
212+
)
213+
argparser.set_defaults(push=None)
184214

185215
argparser.add_argument(
186216
"--volume",
@@ -250,6 +280,7 @@ def get_argparser():
250280
return argparser
251281

252282

283+
# Note: only used by sphinx-autoprogram
253284
argparser = get_argparser()
254285

255286

@@ -274,12 +305,18 @@ def make_r2d(argv=None):
274305
args, traitlet_args = argparser.parse_known_args(argv)
275306

276307
r2d = Repo2Docker()
277-
r2d.parse_command_line(traitlet_args)
278308

279309
if args.debug:
280310
r2d.log_level = logging.DEBUG
281311

312+
# load CLI after config file, for correct priority
282313
r2d.load_config_file(args.config)
314+
r2d.parse_command_line(traitlet_args)
315+
316+
if args.debug:
317+
# re-apply debug in case log_level was also set via config
318+
r2d.log_level = logging.DEBUG
319+
283320
if args.appendix:
284321
r2d.appendix = args.appendix
285322

@@ -291,8 +328,14 @@ def make_r2d(argv=None):
291328
key, _, val = a.partition("=")
292329
r2d.extra_build_args[key] = val
293330

331+
# repo is a required arg, and should never come from config:
332+
if "repo" in r2d.config.Repo2Docker:
333+
r2d.log.warning(
334+
f"Ignoring Repo2Docker.repo={r2d.repo!r} configuration, using {args.repo!r} from CLI."
335+
)
294336
r2d.repo = args.repo
295-
r2d.ref = args.ref
337+
if args.ref is not None:
338+
r2d.ref = args.ref
296339

297340
# user wants to mount a local directory into the container for
298341
# editing
@@ -313,21 +356,22 @@ def make_r2d(argv=None):
313356

314357
if args.image_name:
315358
r2d.output_image_spec = args.image_name
316-
else:
317-
# we will pick a name after fetching the repository
318-
r2d.output_image_spec = ""
319359

320-
r2d.json_logs = args.json_logs
360+
if args.json_logs is not None:
361+
r2d.json_logs = args.json_logs
321362

322-
r2d.dry_run = not args.build
363+
if args.build is not None:
364+
r2d.dry_run = not args.build
323365

324366
if r2d.dry_run:
325367
# Can't push nor run if we aren't building
326368
args.run = False
327369
args.push = False
328370

329-
r2d.run = args.run
330-
r2d.push = args.push
371+
if args.run is not None:
372+
r2d.run = args.run
373+
if args.push is not None:
374+
r2d.push = args.push
331375

332376
# check against r2d.run and not args.run as r2d.run is false on
333377
# --no-build. Also r2d.volumes and not args.volumes since --editable
@@ -341,29 +385,33 @@ def make_r2d(argv=None):
341385
src, dest = v.split(":")
342386
r2d.volumes[src] = dest
343387

344-
r2d.run_cmd = args.cmd
388+
if args.cmd:
389+
r2d.run_cmd = args.cmd
345390

346391
if args.all_ports and not r2d.run:
347-
print(
348-
"To publish user defined port mappings, the container must " "also be run"
349-
)
392+
print("To publish user-defined port mappings, the container must also be run")
350393
sys.exit(1)
351394

352395
if args.ports and not r2d.run:
353-
print(
354-
"To publish user defined port mappings, the container must " "also be run"
355-
)
396+
print("To publish user-defined port mappings, the container must also be run")
356397
sys.exit(1)
357398

358399
if args.ports and len(args.ports) > 1 and not r2d.run_cmd:
359400
print(
360-
"To publish user defined port mapping, user must specify "
361-
"the command to run in the container"
401+
"To publish user-defined port mapping, "
402+
"you must specify the command to run in the container"
362403
)
363404
sys.exit(1)
364405

365-
r2d.ports = validate_and_generate_port_mapping(args.ports)
366-
r2d.all_ports = args.all_ports
406+
if args.ports:
407+
# override or update, if also defined in config file?
408+
if r2d.ports:
409+
r2d.log.warning(
410+
f"Ignoring port configuration from config (ports={r2d.ports}), overridden by CLI"
411+
)
412+
r2d.ports = validate_and_generate_port_mapping(args.ports)
413+
if args.all_ports is not None:
414+
r2d.all_ports = args.all_ports
367415

368416
if args.user_id:
369417
r2d.user_id = args.user_id
@@ -401,13 +449,11 @@ def make_r2d(argv=None):
401449
if args.engine:
402450
r2d.engine = args.engine
403451

404-
r2d.environment = args.environment
452+
if args.environment:
453+
# extend any environment config from a config file
454+
r2d.environment.extend(args.environment)
405455

406-
# if the source exists locally we don't want to delete it at the end
407-
# FIXME: Find a better way to figure out if repo is 'local'. Push this into ContentProvider?
408-
if os.path.exists(args.repo):
409-
r2d.cleanup_checkout = False
410-
else:
456+
elif args.clean is not None:
411457
r2d.cleanup_checkout = args.clean
412458

413459
if args.target_repo_dir:

repo2docker/app.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import entrypoints
2121
import escapism
2222
from pythonjsonlogger import jsonlogger
23-
from traitlets import Any, Bool, Dict, Int, List, Unicode, default
23+
from traitlets import Any, Bool, Dict, Int, List, Unicode, default, observe
2424
from traitlets.config import Application
2525

2626
from . import __version__, contentproviders
@@ -304,7 +304,7 @@ def _user_name_default(self):
304304
)
305305

306306
cleanup_checkout = Bool(
307-
False,
307+
True,
308308
help="""
309309
Delete source repository after building is done.
310310
@@ -313,6 +313,12 @@ def _user_name_default(self):
313313
config=True,
314314
)
315315

316+
@default("cleanup_checkout")
317+
def _defaut_cleanup_checkout(self):
318+
# if the source exists locally we don't want to delete it at the end
319+
# FIXME: Find a better way to figure out if repo is 'local'. Push this into ContentProvider?
320+
return not os.path.exists(self.repo)
321+
316322
output_image_spec = Unicode(
317323
"",
318324
help="""
@@ -332,7 +338,7 @@ def _user_name_default(self):
332338
)
333339

334340
run = Bool(
335-
False,
341+
True,
336342
help="""
337343
Run docker image after building
338344
""",
@@ -349,6 +355,12 @@ def _user_name_default(self):
349355
config=True,
350356
)
351357

358+
@observe("dry_run")
359+
def _dry_run_changed(self, change):
360+
if change.new:
361+
# dry_run forces run and push to be False
362+
self.push = self.run = False
363+
352364
# FIXME: Refactor classes to separate build & run steps
353365
run_cmd = List(
354366
[],
@@ -727,6 +739,8 @@ def build(self):
727739
# making a copy of it as it might contain large files that would be
728740
# expensive to copy.
729741
if os.path.isdir(self.repo):
742+
# never cleanup when we are working in a local repo
743+
self.cleanup_checkout = False
730744
checkout_path = self.repo
731745
else:
732746
if self.git_workdir is None:
@@ -828,6 +842,7 @@ def build(self):
828842

829843
finally:
830844
# Cleanup checkout if necessary
845+
# never cleanup when checking out a local repo
831846
if self.cleanup_checkout:
832847
shutil.rmtree(checkout_path, ignore_errors=True)
833848

tests/unit/test_args.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,36 @@ def test_invalid_image_name():
9595
"""
9696
with pytest.raises(SystemExit):
9797
make_r2d(["--image-name", "_invalid", "."])
98+
99+
100+
def test_build_config_priority(tmp_path):
101+
config_file = str(tmp_path.joinpath("repo2docker_config.py"))
102+
with open(config_file, "w") as f:
103+
f.write("c.Repo2Docker.dry_run = True")
104+
r2d = make_r2d(["--config", config_file, "."])
105+
assert r2d.dry_run
106+
r2d = make_r2d(["--config", config_file, "--build", "."])
107+
assert not r2d.dry_run
108+
with open(config_file, "w") as f:
109+
f.write("c.Repo2Docker.dry_run = False")
110+
r2d = make_r2d(["--config", config_file, "--no-build", "."])
111+
assert r2d.dry_run
112+
113+
114+
@pytest.mark.parametrize(
115+
"trait, arg, default",
116+
[
117+
("output_image_spec", "--image-name", ""),
118+
("ref", "--ref", None),
119+
],
120+
)
121+
def test_config_priority(tmp_path, trait, arg, default):
122+
config_file = str(tmp_path.joinpath("repo2docker_config.py"))
123+
r2d = make_r2d(["."])
124+
assert getattr(r2d, trait) == default
125+
with open(config_file, "w") as f:
126+
f.write(f"c.Repo2Docker.{trait} = 'config'")
127+
r2d = make_r2d(["--config", config_file, "."])
128+
assert getattr(r2d, trait) == "config"
129+
r2d = make_r2d(["--config", config_file, arg, "cli", "."])
130+
assert getattr(r2d, trait) == "cli"

tests/unit/test_argumentvalidation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ def test_port_mapping_no_run_fail(temp_cwd):
162162
assert not validate_arguments(
163163
builddir,
164164
args_list,
165-
"To publish user defined port mappings, the container must also be run",
165+
"To publish user-defined port mappings, the container must also be run",
166166
)
167167

168168

@@ -175,7 +175,7 @@ def test_all_ports_mapping_no_run_fail(temp_cwd):
175175
assert not validate_arguments(
176176
builddir,
177177
args_list,
178-
"To publish user defined port mappings, the container must also be run",
178+
"To publish user-defined port mappings, the container must also be run",
179179
)
180180

181181

0 commit comments

Comments
 (0)