Skip to content

Commit

Permalink
Lean on pkgutil.resolve_name for importing apps
Browse files Browse the repository at this point in the history
With #445, it no longer makes sense not to use `pkgutil.resolve_name` to
handle the importing of WSGI applications in the runner. There is one
very minor behavioural change in that `pkgutil.resolve_name` allows a
broader syntax for the imports, but supports the same syntax as the
existing code being removed, so this is a minor concern.

It has the pleasant side effect of removing some legacy code only
present for Python 2.6 that was missed when Python 2 support was
removed.
  • Loading branch information
kgaughan committed Oct 27, 2024
1 parent fdd2ecf commit f8fcd34
Show file tree
Hide file tree
Showing 2 changed files with 10 additions and 116 deletions.
60 changes: 4 additions & 56 deletions src/waitress/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import logging
import os
import os.path
import re
import pkgutil
import sys

from waitress import serve
Expand Down Expand Up @@ -179,47 +179,6 @@
"""

RUNNER_PATTERN = re.compile(
r"""
^
(?P<module>
[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*
)
:
(?P<object>
[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*
)
$
""",
re.I | re.X,
)


def match(obj_name):
matches = RUNNER_PATTERN.match(obj_name)
if not matches:
raise ValueError(f"Malformed application '{obj_name}'")
return matches.group("module"), matches.group("object")


def resolve(module_name, object_name):
"""Resolve a named object in a module."""
# We cast each segments due to an issue that has been found to manifest
# in Python 2.6.6, but not 2.6.8, and may affect other revisions of Python
# 2.6 and 2.7, whereby ``__import__`` chokes if the list passed in the
# ``fromlist`` argument are unicode strings rather than 8-bit strings.
# The error triggered is "TypeError: Item in ``fromlist '' not a string".
# My guess is that this was fixed by checking against ``basestring``
# rather than ``str`` sometime between the release of 2.6.6 and 2.6.8,
# but I've yet to go over the commits. I know, however, that the NEWS
# file makes no mention of such a change to the behaviour of
# ``__import__``.
segments = [str(segment) for segment in object_name.split(".")]
obj = __import__(module_name, fromlist=segments[:1])
for segment in segments:
obj = getattr(obj, segment)
return obj


def show_help(stream, name, error=None): # pragma: no cover
if error is not None:
Expand Down Expand Up @@ -268,25 +227,14 @@ def run(argv=sys.argv, _serve=serve):
if logger.level == logging.NOTSET:
logger.setLevel(logging.INFO)

try:
module, obj_name = match(args[0])
except ValueError as exc:
show_help(sys.stderr, name, str(exc))
show_exception(sys.stderr)
return 1

# Add the current directory onto sys.path
sys.path.append(os.getcwd())

# Get the WSGI function.
try:
app = resolve(module, obj_name)
except ImportError:
show_help(sys.stderr, name, f"Bad module '{module}'")
show_exception(sys.stderr)
return 1
except AttributeError:
show_help(sys.stderr, name, f"Bad object name '{obj_name}'")
app = pkgutil.resolve_name(args[0])
except (ValueError, ImportError, AttributeError) as exc:
show_help(sys.stderr, name, str(exc))
show_exception(sys.stderr)
return 1
if kw["call"]:
Expand Down
66 changes: 6 additions & 60 deletions tests/test_runner.py
Original file line number Diff line number Diff line change
@@ -1,67 +1,11 @@
import contextlib
import os
import sys

if sys.version_info[:2] == (2, 6): # pragma: no cover
import unittest2 as unittest
else: # pragma: no cover
import unittest
import unittest

from waitress import runner


class Test_match(unittest.TestCase):
def test_empty(self):
self.assertRaisesRegex(
ValueError, "^Malformed application ''$", runner.match, ""
)

def test_module_only(self):
self.assertRaisesRegex(
ValueError, r"^Malformed application 'foo\.bar'$", runner.match, "foo.bar"
)

def test_bad_module(self):
self.assertRaisesRegex(
ValueError,
r"^Malformed application 'foo#bar:barney'$",
runner.match,
"foo#bar:barney",
)

def test_module_obj(self):
self.assertTupleEqual(
runner.match("foo.bar:fred.barney"), ("foo.bar", "fred.barney")
)


class Test_resolve(unittest.TestCase):
def test_bad_module(self):
self.assertRaises(
ImportError, runner.resolve, "nonexistent", "nonexistent_function"
)

def test_nonexistent_function(self):
self.assertRaisesRegex(
AttributeError,
r"has no attribute 'nonexistent_function'",
runner.resolve,
"os.path",
"nonexistent_function",
)

def test_simple_happy_path(self):
from os.path import exists

self.assertIs(runner.resolve("os.path", "exists"), exists)

def test_complex_happy_path(self):
# Ensure we can recursively resolve object attributes if necessary.
from os.path import exists

self.assertEqual(runner.resolve("os.path", "exists.__name__"), exists.__name__)


class Test_run(unittest.TestCase):
def match_output(self, argv, code, regex):
argv = ["waitress-serve"] + argv
Expand All @@ -83,10 +27,10 @@ def test_multiple_apps_app(self):
self.match_output(["a:a", "b:b"], 1, "^Error: Specify one application only")

def test_bad_apps_app(self):
self.match_output(["a"], 1, "^Error: Malformed application 'a'")
self.match_output(["a"], 1, "^Error: No module named 'a'")

def test_bad_app_module(self):
self.match_output(["nonexistent:a"], 1, "^Error: Bad module 'nonexistent'")
self.match_output(["nonexistent:a"], 1, "^Error: No module named 'nonexistent'")

self.match_output(
["nonexistent:a"],
Expand Down Expand Up @@ -117,7 +61,9 @@ def null_serve(app, **kw):

def test_bad_app_object(self):
self.match_output(
["tests.fixtureapps.runner:a"], 1, "^Error: Bad object name 'a'"
["tests.fixtureapps.runner:a"],
1,
"^Error: module 'tests.fixtureapps.runner' has no attribute 'a'",
)

def test_simple_call(self):
Expand Down

0 comments on commit f8fcd34

Please sign in to comment.