GeezerVison is showing github.com — gh-68583: webbrowser: replace `getopt` with `argparse`, add long opti… · diegorusso/cpython@7c2eb8a
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: Doc/library/webbrowser.rst +6-3Lines changed: 6 additions & 3 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -42,9 +42,12 @@ a new tab, with the browser being brought to the foreground. The use of the | 42 | 42 |
| 43 | 43 | The script :program:`webbrowser` can be used as a command-line interface for the | 44 | 44 | module. It accepts a URL as the argument. It accepts the following optional | 45 | | -parameters: ``-n`` opens the URL in a new browser window, if possible; | 46 | | -``-t`` opens the URL in a new browser page ("tab"). The options are, | 47 | | -naturally, mutually exclusive. Usage example:: | | 45 | +parameters: | | 46 | + | | 47 | +* ``-n``/``--new-window`` opens the URL in a new browser window, if possible. | | 48 | +* ``-t``/``--new-tab`` opens the URL in a new browser page ("tab"). | | 49 | + | | 50 | +The options are, naturally, mutually exclusive. Usage example:: | 48 | 51 |
| 49 | 52 | python -m webbrowser -t "https://www.python.org" | 50 | 53 |
| |
Collapse file Copy file name to clipboardExpand all lines: Lib/test/test_webbrowser.py +78-11Lines changed: 78 additions & 11 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | | -import webbrowser | 2 | | -import unittest | 3 | 1 | import os | 4 | | -import sys | | 2 | +import re | | 3 | +import shlex | 5 | 4 | import subprocess | 6 | | -from unittest import mock | | 5 | +import sys | | 6 | +import unittest | | 7 | +import webbrowser | 7 | 8 | from test import support | 8 | | -from test.support import is_apple_mobile | 9 | 9 | from test.support import import_helper | | 10 | +from test.support import is_apple_mobile | 10 | 11 | from test.support import os_helper | 11 | 12 | from test.support import requires_subprocess | 12 | 13 | from test.support import threading_helper | | 14 | +from unittest import mock | 13 | 15 |
| 14 | 16 | # The webbrowser module uses threading locks | 15 | 17 | threading_helper.requires_working_threading(module=True) | @@ -98,6 +100,15 @@ def test_open_new_tab(self): | 98 | 100 | options=[], | 99 | 101 | arguments=[URL]) | 100 | 102 |
| | 103 | + def test_open_bad_new_parameter(self): | | 104 | + with self.assertRaisesRegex(webbrowser.Error, | | 105 | + re.escape("Bad 'new' parameter to open(); " | | 106 | + "expected 0, 1, or 2, got 999")): | | 107 | + self._test('open', | | 108 | + options=[], | | 109 | + arguments=[URL], | | 110 | + kw=dict(new=999)) | | 111 | + | 101 | 112 |
| 102 | 113 | class EdgeCommandTest(CommandTestMixin, unittest.TestCase): | 103 | 114 |
| @@ -205,22 +216,22 @@ class ELinksCommandTest(CommandTestMixin, unittest.TestCase): | 205 | 216 |
| 206 | 217 | def test_open(self): | 207 | 218 | self._test('open', options=['-remote'], | 208 | | - arguments=['openURL({})'.format(URL)]) | | 219 | + arguments=[f'openURL({URL})']) | 209 | 220 |
| 210 | 221 | def test_open_with_autoraise_false(self): | 211 | 222 | self._test('open', | 212 | 223 | options=['-remote'], | 213 | | - arguments=['openURL({})'.format(URL)]) | | 224 | + arguments=[f'openURL({URL})']) | 214 | 225 |
| 215 | 226 | def test_open_new(self): | 216 | 227 | self._test('open_new', | 217 | 228 | options=['-remote'], | 218 | | - arguments=['openURL({},new-window)'.format(URL)]) | | 229 | + arguments=[f'openURL({URL},new-window)']) | 219 | 230 |
| 220 | 231 | def test_open_new_tab(self): | 221 | 232 | self._test('open_new_tab', | 222 | 233 | options=['-remote'], | 223 | | - arguments=['openURL({},new-tab)'.format(URL)]) | | 234 | + arguments=[f'openURL({URL},new-tab)']) | 224 | 235 |
| 225 | 236 |
| 226 | 237 | @unittest.skipUnless(sys.platform == "ios", "Test only applicable to iOS") | @@ -342,7 +353,6 @@ def test_register_default(self): | 342 | 353 | def test_register_preferred(self): | 343 | 354 | self._check_registration(preferred=True) | 344 | 355 |
| 345 | | - | 346 | 356 | @unittest.skipUnless(sys.platform == "darwin", "macOS specific test") | 347 | 357 | def test_no_xdg_settings_on_macOS(self): | 348 | 358 | # On macOS webbrowser should not use xdg-settings to | @@ -423,5 +433,62 @@ def test_environment_preferred(self): | 423 | 433 | self.assertEqual(webbrowser.get().name, sys.executable) | 424 | 434 |
| 425 | 435 |
| 426 | | -if __name__=='__main__': | | 436 | +class CliTest(unittest.TestCase): | | 437 | + def test_parse_args(self): | | 438 | + for command, url, new_win in [ | | 439 | + # No optional arguments | | 440 | + ("https://example.com", "https://example.com", 0), | | 441 | + # Each optional argument | | 442 | + ("https://example.com -n", "https://example.com", 1), | | 443 | + ("-n https://example.com", "https://example.com", 1), | | 444 | + ("https://example.com -t", "https://example.com", 2), | | 445 | + ("-t https://example.com", "https://example.com", 2), | | 446 | + # Long form | | 447 | + ("https://example.com --new-window", "https://example.com", 1), | | 448 | + ("--new-window https://example.com", "https://example.com", 1), | | 449 | + ("https://example.com --new-tab", "https://example.com", 2), | | 450 | + ("--new-tab https://example.com", "https://example.com", 2), | | 451 | + ]: | | 452 | + args = webbrowser.parse_args(shlex.split(command)) | | 453 | + | | 454 | + self.assertEqual(args.url, url) | | 455 | + self.assertEqual(args.new_win, new_win) | | 456 | + | | 457 | + def test_parse_args_error(self): | | 458 | + for command in [ | | 459 | + # Arguments must not both be given | | 460 | + "https://example.com -n -t", | | 461 | + "https://example.com --new-window --new-tab", | | 462 | + "https://example.com -n --new-tab", | | 463 | + "https://example.com --new-window -t", | | 464 | + # Ensure ambiguous shortening fails | | 465 | + "https://example.com --new", | | 466 | + ]: | | 467 | + with self.assertRaises(SystemExit): | | 468 | + webbrowser.parse_args(shlex.split(command)) | | 469 | + | | 470 | + def test_main(self): | | 471 | + for command, expected_url, expected_new_win in [ | | 472 | + # No optional arguments | | 473 | + ("https://example.com", "https://example.com", 0), | | 474 | + # Each optional argument | | 475 | + ("https://example.com -n", "https://example.com", 1), | | 476 | + ("-n https://example.com", "https://example.com", 1), | | 477 | + ("https://example.com -t", "https://example.com", 2), | | 478 | + ("-t https://example.com", "https://example.com", 2), | | 479 | + # Long form | | 480 | + ("https://example.com --new-window", "https://example.com", 1), | | 481 | + ("--new-window https://example.com", "https://example.com", 1), | | 482 | + ("https://example.com --new-tab", "https://example.com", 2), | | 483 | + ("--new-tab https://example.com", "https://example.com", 2), | | 484 | + ]: | | 485 | + with ( | | 486 | + mock.patch("webbrowser.open", return_value=None) as mock_open, | | 487 | + mock.patch("builtins.print", return_value=None), | | 488 | + ): | | 489 | + webbrowser.main(shlex.split(command)) | | 490 | + mock_open.assert_called_once_with(expected_url, expected_new_win) | | 491 | + | | 492 | + | | 493 | +if __name__ == '__main__': | 427 | 494 | unittest.main() |
Collapse file Copy file name to clipboardExpand all lines: Lib/webbrowser.py +49-41Lines changed: 49 additions & 41 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 11 | 11 |
| 12 | 12 | __all__ = ["Error", "open", "open_new", "open_new_tab", "get", "register"] | 13 | 13 |
| | 14 | + | 14 | 15 | class Error(Exception): | 15 | 16 | pass | 16 | 17 |
| | 18 | + | 17 | 19 | _lock = threading.RLock() | 18 | 20 | _browsers = {} # Dictionary of available browser controllers | 19 | 21 | _tryorder = None # Preference order of available browsers | 20 | 22 | _os_preferred_browser = None # The preferred browser | 21 | 23 |
| | 24 | + | 22 | 25 | def register(name, klass, instance=None, *, preferred=False): | 23 | 26 | """Register a browser connector.""" | 24 | 27 | with _lock: | @@ -34,6 +37,7 @@ def register(name, klass, instance=None, *, preferred=False): | 34 | 37 | else: | 35 | 38 | _tryorder.append(name) | 36 | 39 |
| | 40 | + | 37 | 41 | def get(using=None): | 38 | 42 | """Return a browser launcher instance appropriate for the environment.""" | 39 | 43 | if _tryorder is None: | @@ -64,6 +68,7 @@ def get(using=None): | 64 | 68 | return command[0]() | 65 | 69 | raise Error("could not locate runnable browser") | 66 | 70 |
| | 71 | + | 67 | 72 | # Please note: the following definition hides a builtin function. | 68 | 73 | # It is recommended one does "import webbrowser" and uses webbrowser.open(url) | 69 | 74 | # instead of "from webbrowser import *". | @@ -87,13 +92,15 @@ def open(url, new=0, autoraise=True): | 87 | 92 | return True | 88 | 93 | return False | 89 | 94 |
| | 95 | + | 90 | 96 | def open_new(url): | 91 | 97 | """Open url in a new window of the default browser. | 92 | 98 |
| 93 | 99 | If not possible, then open url in the only browser window. | 94 | 100 | """ | 95 | 101 | return open(url, 1) | 96 | 102 |
| | 103 | + | 97 | 104 | def open_new_tab(url): | 98 | 105 | """Open url in a new page ("tab") of the default browser. | 99 | 106 |
| @@ -136,7 +143,7 @@ def _synthesize(browser, *, preferred=False): | 136 | 143 |
| 137 | 144 | # General parent classes | 138 | 145 |
| 139 | | -class BaseBrowser(object): | | 146 | +class BaseBrowser: | 140 | 147 | """Parent class for all browsers. Do not use directly.""" | 141 | 148 |
| 142 | 149 | args = ['%s'] | @@ -197,7 +204,7 @@ def open(self, url, new=0, autoraise=True): | 197 | 204 | else: | 198 | 205 | p = subprocess.Popen(cmdline, close_fds=True, | 199 | 206 | start_new_session=True) | 200 | | - return (p.poll() is None) | | 207 | + return p.poll() is None | 201 | 208 | except OSError: | 202 | 209 | return False | 203 | 210 |
| @@ -225,7 +232,8 @@ def _invoke(self, args, remote, autoraise, url=None): | 225 | 232 | # use autoraise argument only for remote invocation | 226 | 233 | autoraise = int(autoraise) | 227 | 234 | opt = self.raise_opts[autoraise] | 228 | | - if opt: raise_opt = [opt] | | 235 | + if opt: | | 236 | + raise_opt = [opt] | 229 | 237 |
| 230 | 238 | cmdline = [self.name] + raise_opt + args | 231 | 239 |
| @@ -266,8 +274,8 @@ def open(self, url, new=0, autoraise=True): | 266 | 274 | else: | 267 | 275 | action = self.remote_action_newtab | 268 | 276 | else: | 269 | | - raise Error("Bad 'new' parameter to open(); " + | 270 | | - "expected 0, 1, or 2, got %s" % new) | | 277 | + raise Error("Bad 'new' parameter to open(); " | | 278 | + f"expected 0, 1, or 2, got {new}") | 271 | 279 |
| 272 | 280 | args = [arg.replace("%s", url).replace("%action", action) | 273 | 281 | for arg in self.remote_args] | @@ -302,19 +310,20 @@ class Epiphany(UnixBrowser): | 302 | 310 |
| 303 | 311 |
| 304 | 312 | class Chrome(UnixBrowser): | 305 | | - "Launcher class for Google Chrome browser." | | 313 | + """Launcher class for Google Chrome browser.""" | 306 | 314 |
| 307 | 315 | remote_args = ['%action', '%s'] | 308 | 316 | remote_action = "" | 309 | 317 | remote_action_newwin = "--new-window" | 310 | 318 | remote_action_newtab = "" | 311 | 319 | background = True | 312 | 320 |
| | 321 | + | 313 | 322 | Chromium = Chrome | 314 | 323 |
| 315 | 324 |
| 316 | 325 | class Opera(UnixBrowser): | 317 | | - "Launcher class for Opera browser." | | 326 | + """Launcher class for Opera browser.""" | 318 | 327 |
| 319 | 328 | remote_args = ['%action', '%s'] | 320 | 329 | remote_action = "" | @@ -324,7 +333,7 @@ class Opera(UnixBrowser): | 324 | 333 |
| 325 | 334 |
| 326 | 335 | class Elinks(UnixBrowser): | 327 | | - "Launcher class for Elinks browsers." | | 336 | + """Launcher class for Elinks browsers.""" | 328 | 337 |
| 329 | 338 | remote_args = ['-remote', 'openURL(%s%action)'] | 330 | 339 | remote_action = "" | @@ -387,11 +396,11 @@ def open(self, url, new=0, autoraise=True): | 387 | 396 | except OSError: | 388 | 397 | return False | 389 | 398 | else: | 390 | | - return (p.poll() is None) | | 399 | + return p.poll() is None | 391 | 400 |
| 392 | 401 |
| 393 | 402 | class Edge(UnixBrowser): | 394 | | - "Launcher class for Microsoft Edge browser." | | 403 | + """Launcher class for Microsoft Edge browser.""" | 395 | 404 |
| 396 | 405 | remote_args = ['%action', '%s'] | 397 | 406 | remote_action = "" | @@ -461,7 +470,6 @@ def register_X_browsers(): | 461 | 470 | if shutil.which("opera"): | 462 | 471 | register("opera", None, Opera("opera")) | 463 | 472 |
| 464 | | - | 465 | 473 | if shutil.which("microsoft-edge"): | 466 | 474 | register("microsoft-edge", None, Edge("microsoft-edge")) | 467 | 475 |
| @@ -514,7 +522,8 @@ def register_standard_browsers(): | 514 | 522 | cmd = "xdg-settings get default-web-browser".split() | 515 | 523 | raw_result = subprocess.check_output(cmd, stderr=subprocess.DEVNULL) | 516 | 524 | result = raw_result.decode().strip() | 517 | | - except (FileNotFoundError, subprocess.CalledProcessError, PermissionError, NotADirectoryError) : | | 525 | + except (FileNotFoundError, subprocess.CalledProcessError, | | 526 | + PermissionError, NotADirectoryError): | 518 | 527 | pass | 519 | 528 | else: | 520 | 529 | global _os_preferred_browser | @@ -584,15 +593,16 @@ def __init__(self, name='default'): | 584 | 593 |
| 585 | 594 | def open(self, url, new=0, autoraise=True): | 586 | 595 | sys.audit("webbrowser.open", url) | | 596 | + url = url.replace('"', '%22') | 587 | 597 | if self.name == 'default': | 588 | | - script = 'open location "%s"' % url.replace('"', '%22') # opens in default browser | | 598 | + script = f'open location "{url}"' # opens in default browser | 589 | 599 | else: | 590 | 600 | script = f''' | 591 | | - tell application "%s" | | 601 | + tell application "{self.name}" | 592 | 602 | activate | 593 | | - open location "%s" | | 603 | + open location "{url}" | 594 | 604 | end | 595 | | - '''%(self.name, url.replace('"', '%22')) | | 605 | + ''' | 596 | 606 |
| 597 | 607 | osapipe = os.popen("osascript", "w") | 598 | 608 | if osapipe is None: | @@ -667,33 +677,31 @@ def open(self, url, new=0, autoraise=True): | 667 | 677 | return True | 668 | 678 |
| 669 | 679 |
| 670 | | -def main(): | 671 | | - import getopt | 672 | | - usage = """Usage: %s [-n | -t | -h] url | 673 | | - -n: open new window | 674 | | - -t: open new tab | 675 | | - -h, --help: show help""" % sys.argv[0] | 676 | | - try: | 677 | | - opts, args = getopt.getopt(sys.argv[1:], 'ntdh',['help']) | 678 | | - except getopt.error as msg: | 679 | | - print(msg, file=sys.stderr) | 680 | | - print(usage, file=sys.stderr) | 681 | | - sys.exit(1) | 682 | | - new_win = 0 | 683 | | - for o, a in opts: | 684 | | - if o == '-n': new_win = 1 | 685 | | - elif o == '-t': new_win = 2 | 686 | | - elif o == '-h' or o == '--help': | 687 | | - print(usage, file=sys.stderr) | 688 | | - sys.exit() | 689 | | - if len(args) != 1: | 690 | | - print(usage, file=sys.stderr) | 691 | | - sys.exit(1) | 692 | | - | 693 | | - url = args[0] | 694 | | - open(url, new_win) | | 680 | +def parse_args(arg_list: list[str] | None): | | 681 | + import argparse | | 682 | + parser = argparse.ArgumentParser(description="Open URL in a web browser.") | | 683 | + parser.add_argument("url", help="URL to open") | | 684 | + | | 685 | + group = parser.add_mutually_exclusive_group() | | 686 | + group.add_argument("-n", "--new-window", action="store_const", | | 687 | + const=1, default=0, dest="new_win", | | 688 | + help="open new window") | | 689 | + group.add_argument("-t", "--new-tab", action="store_const", | | 690 | + const=2, default=0, dest="new_win", | | 691 | + help="open new tab") | | 692 | + | | 693 | + args = parser.parse_args(arg_list) | | 694 | + | | 695 | + return args | | 696 | + | | 697 | + | | 698 | +def main(arg_list: list[str] | None = None): | | 699 | + args = parse_args(arg_list) | | 700 | + | | 701 | + open(args.url, args.new_win) | 695 | 702 |
| 696 | 703 | print("\a") | 697 | 704 |
| | 705 | + | 698 | 706 | if __name__ == "__main__": | 699 | 707 | main() |
Collapse file Copy file name to clipboard +2Lines changed: 2 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +webbrowser CLI: replace getopt with argparse, add long options. Patch by | | 2 | +Hugo van Kemenade. |
|