Skip to content

Commit 5cceabc

Browse files
committed
Move app resolution into parse_cli; deal with multiple apps more cleanly
1 parent d70c0d6 commit 5cceabc

File tree

4 files changed

+79
-65
lines changed

4 files changed

+79
-65
lines changed

src/waitress/adjustments.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""Adjustments are tunable parameters.
1515
"""
1616
import getopt
17+
import pkgutil
1718
import socket
1819
import warnings
1920

@@ -95,6 +96,10 @@ class _int_marker(int):
9596
pass
9697

9798

99+
class AppResolutionError(Exception):
100+
"""The named WSGI application could not be resolved."""
101+
102+
98103
class Adjustments:
99104
"""This class contains tunable parameters."""
100105

@@ -467,6 +472,7 @@ def parse_args(cls, argv):
467472
"app": None,
468473
}
469474

475+
app = None
470476
opts, args = getopt.getopt(argv, "", long_opts)
471477
for opt, value in opts:
472478
param = opt.lstrip("-").replace("-", "_")
@@ -481,16 +487,31 @@ def parse_args(cls, argv):
481487
elif param in ("help", "call"):
482488
kw[param] = True
483489
elif param == "app":
484-
kw[param] = value
490+
app = value
485491
elif cls._param_map[param] is asbool:
486492
kw[param] = "true"
487493
else:
488494
kw[param] = value
489495

490-
if kw["app"] is None and len(args) > 0:
491-
kw["app"] = args.pop(0)
496+
if not kw["help"]:
497+
if app is None and len(args) > 0:
498+
app = args.pop(0)
499+
if app is None:
500+
raise AppResolutionError("Specify an application")
501+
if len(args) > 0:
502+
raise AppResolutionError("Provide only one WSGI app")
503+
504+
# Get the WSGI function.
505+
try:
506+
kw["app"] = pkgutil.resolve_name(app)
507+
except (ValueError, ImportError, AttributeError) as exc:
508+
raise AppResolutionError(f"Cannot import WSGI application: {exc}")
509+
if kw["call"]:
510+
kw["app"] = kw["app"]()
511+
512+
del kw["call"]
492513

493-
return kw, args
514+
return kw
494515

495516
@classmethod
496517
def check_sockets(cls, sockets):

src/waitress/runner.py

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,16 @@
1111
# FOR A PARTICULAR PURPOSE.
1212
#
1313
##############################################################################
14-
"""Command line runner.
15-
"""
16-
14+
"""Command line runner."""
1715

1816
import getopt
1917
import logging
2018
import os
2119
import os.path
22-
import pkgutil
2320
import sys
2421

2522
from waitress import serve
26-
from waitress.adjustments import Adjustments
23+
from waitress.adjustments import Adjustments, AppResolutionError
2724
from waitress.utilities import logger
2825

2926
HELP = """\
@@ -285,42 +282,31 @@ def show_help(stream, name, error=None): # pragma: no cover
285282

286283
def run(argv=sys.argv, _serve=serve):
287284
"""Command line runner."""
285+
# Add the current directory onto sys.path
286+
sys.path.append(os.getcwd())
287+
288288
name = os.path.basename(argv[0])
289289

290290
try:
291-
kw, args = Adjustments.parse_args(argv[1:])
292-
except getopt.GetoptError as exc:
291+
kw = Adjustments.parse_args(argv[1:])
292+
except (getopt.GetoptError, AppResolutionError) as exc:
293293
show_help(sys.stderr, name, str(exc))
294294
return 1
295295

296296
if kw["help"]:
297297
show_help(sys.stdout, name)
298298
return 0
299299

300-
if kw["app"] is None:
301-
show_help(sys.stderr, name, "Specify an application")
302-
return 1
303-
304300
# set a default level for the logger only if it hasn't been set explicitly
305301
# note that this level does not override any parent logger levels,
306302
# handlers, etc but without it no log messages are emitted by default
307303
if logger.level == logging.NOTSET:
308304
logger.setLevel(logging.INFO)
309305

310-
# Add the current directory onto sys.path
311-
sys.path.append(os.getcwd())
312-
313-
# Get the WSGI function.
314-
try:
315-
app = pkgutil.resolve_name(kw["app"])
316-
except (ValueError, ImportError, AttributeError) as exc:
317-
show_help(sys.stderr, name, str(exc))
318-
return 1
319-
if kw["call"]:
320-
app = app()
306+
app = kw["app"]
321307

322308
# These arguments are specific to the runner, not waitress itself.
323-
del kw["call"], kw["help"], kw["app"]
309+
del kw["help"], kw["app"]
324310

325311
_serve(app, **kw)
326312
return 0

tests/test_adjustments.py

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -395,48 +395,57 @@ def assertDictContainsSubset(self, subset, dictionary):
395395
self.assertTrue(set(subset.items()) <= set(dictionary.items()))
396396

397397
def test_noargs(self):
398-
opts, args = self.parse([])
399-
self.assertDictEqual(opts, {"call": False, "help": False, "app": None})
400-
self.assertSequenceEqual(args, [])
398+
from waitress.adjustments import AppResolutionError
399+
400+
self.assertRaises(AppResolutionError, self.parse, [])
401401

402402
def test_help(self):
403-
opts, args = self.parse(["--help"])
404-
self.assertDictEqual(opts, {"call": False, "help": True, "app": None})
405-
self.assertSequenceEqual(args, [])
403+
opts = self.parse(["--help"])
404+
self.assertDictEqual(opts, {"help": True, "app": None})
406405

407-
def test_call(self):
408-
opts, args = self.parse(["--call"])
409-
self.assertDictEqual(opts, {"call": True, "help": False, "app": None})
410-
self.assertSequenceEqual(args, [])
406+
def test_app_flag(self):
407+
from tests.fixtureapps import runner as _apps
411408

412-
def test_both(self):
413-
opts, args = self.parse(["--call", "--help"])
414-
self.assertDictEqual(opts, {"call": True, "help": True, "app": None})
415-
self.assertSequenceEqual(args, [])
409+
opts = self.parse(["--app=tests.fixtureapps.runner:app"])
410+
self.assertEqual(opts["app"], _apps.app)
416411

417-
def test_app_flag(self):
418-
opts, args = self.parse(["--app=fred:wilma", "barney:betty"])
419-
self.assertEqual(opts["app"], "fred:wilma")
420-
self.assertSequenceEqual(args, ["barney:betty"])
412+
def test_call(self):
413+
from tests.fixtureapps import runner as _apps
414+
415+
opts = self.parse(["--app=tests.fixtureapps.runner:returns_app", "--call"])
416+
self.assertEqual(opts["app"], _apps.app)
421417

422418
def test_app_arg(self):
423-
opts, args = self.parse(["barney:betty"])
424-
self.assertEqual(opts["app"], "barney:betty")
425-
self.assertSequenceEqual(args, [])
419+
from tests.fixtureapps import runner as _apps
420+
421+
opts = self.parse(["tests.fixtureapps.runner:app"])
422+
self.assertEqual(opts["app"], _apps.app)
423+
424+
def test_excess(self):
425+
from waitress.adjustments import AppResolutionError
426+
427+
self.assertRaises(
428+
AppResolutionError,
429+
self.parse,
430+
["tests.fixtureapps.runner:app", "tests.fixtureapps.runner:app"],
431+
)
426432

427433
def test_positive_boolean(self):
428-
opts, args = self.parse(["--expose-tracebacks"])
434+
opts = self.parse(["--expose-tracebacks", "tests.fixtureapps.runner:app"])
429435
self.assertDictContainsSubset({"expose_tracebacks": "true"}, opts)
430-
self.assertSequenceEqual(args, [])
431436

432437
def test_negative_boolean(self):
433-
opts, args = self.parse(["--no-expose-tracebacks"])
438+
opts = self.parse(["--no-expose-tracebacks", "tests.fixtureapps.runner:app"])
434439
self.assertDictContainsSubset({"expose_tracebacks": "false"}, opts)
435-
self.assertSequenceEqual(args, [])
436440

437441
def test_cast_params(self):
438-
opts, args = self.parse(
439-
["--host=localhost", "--port=80", "--unix-socket-perms=777"]
442+
opts = self.parse(
443+
[
444+
"--host=localhost",
445+
"--port=80",
446+
"--unix-socket-perms=777",
447+
"tests.fixtureapps.runner:app",
448+
]
440449
)
441450
self.assertDictContainsSubset(
442451
{
@@ -446,28 +455,25 @@ def test_cast_params(self):
446455
},
447456
opts,
448457
)
449-
self.assertSequenceEqual(args, [])
450458

451459
def test_listen_params(self):
452-
opts, args = self.parse(
460+
opts = self.parse(
453461
[
454462
"--listen=test:80",
463+
"tests.fixtureapps.runner:app",
455464
]
456465
)
457-
458466
self.assertDictContainsSubset({"listen": " test:80"}, opts)
459-
self.assertSequenceEqual(args, [])
460467

461468
def test_multiple_listen_params(self):
462-
opts, args = self.parse(
469+
opts = self.parse(
463470
[
464471
"--listen=test:80",
465472
"--listen=test:8080",
473+
"tests.fixtureapps.runner:app",
466474
]
467475
)
468-
469476
self.assertDictContainsSubset({"listen": " test:80 test:8080"}, opts)
470-
self.assertSequenceEqual(args, [])
471477

472478
def test_bad_param(self):
473479
import getopt

tests/test_runner.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ def test_no_app(self):
2424
self.match_output([], 1, "^Error: Specify an application")
2525

2626
def test_multiple_apps_app(self):
27-
self.match_output(["a:a", "b:b"], 1, "^Error: No module named 'a'")
27+
self.match_output(["a:a", "b:b"], 1, "^Error: Provide only one WSGI app")
28+
self.match_output(["--app=a:a", "b:b"], 1, "^Error: Provide only one WSGI app")
2829

2930
def test_bad_apps_app(self):
30-
self.match_output(["a"], 1, "^Error: No module named 'a'")
31+
self.match_output(["a"], 1, "No module named 'a'")
3132

3233
def test_bad_app_module(self):
33-
self.match_output(["nonexistent:a"], 1, "^Error: No module named 'nonexistent'")
34+
self.match_output(["nonexistent:a"], 1, "No module named 'nonexistent'")
3435

3536
def test_cwd_added_to_path(self):
3637
def null_serve(app, **kw):
@@ -53,7 +54,7 @@ def test_bad_app_object(self):
5354
self.match_output(
5455
["tests.fixtureapps.runner:a"],
5556
1,
56-
"^Error: module 'tests.fixtureapps.runner' has no attribute 'a'",
57+
"module 'tests.fixtureapps.runner' has no attribute 'a'",
5758
)
5859

5960
def test_simple_call(self):

0 commit comments

Comments
 (0)