Merge lp:~jtv/maas/maascli-extract-parser into lp:maas/trunk

Proposed by Jeroen T. Vermeulen on 2012-10-08
Status: Merged
Approved by: Jeroen T. Vermeulen on 2012-10-08
Approved revision: 1224
Merged at revision: 1223
Proposed branch: lp:~jtv/maas/maascli-extract-parser
Merge into: lp:maas/trunk
Diff against target: 226 lines (+110/-47)
5 files modified
src/maascli/__init__.py (+2/-38)
src/maascli/parser.py (+72/-0)
src/maascli/tests/test_api.py (+2/-4)
src/maascli/tests/test_cli.py (+2/-4)
src/maascli/tests/test_parser.py (+32/-1)
To merge this branch: bzr merge lp:~jtv/maas/maascli-extract-parser
Reviewer Review Type Date Requested Status
Gavin Panella (community) 2012-10-08 Approve on 2012-10-08
Review via email: mp+128501@code.launchpad.net

Commit Message

Extract the arguments-parsing part of maascli into its own module.

Description of the Change

This was bloating the diff for my user-interface changes to maascli. In follow-up branches, the parser will learn a new --profile option; register API functions directly into its main namespace; and move the CLI management commands down into a new "cli" namespace.

Jeroen

To post a comment you must log in.
Gavin Panella (allenap) wrote :

Looks good.

[1]

+def get_profile_option(argv):
...
+    specialized_parser = ArgumentParser(add_help=False)
+    specialized_parser.add_argument('--profile', metavar='PROFILE')
+    provisional_options = specialized_parser.parse_known_args(argv)[0]
+    return provisional_options.profile

This throws away the left-over args. I suspect you might want them
later on, otherwise you'll need to allow for parsing the --profile
option again.

[2]

+    module = __import__('maascli.api', fromlist=True)

This is a hang-over from ye olden days; you can probably just import
the module.

review: Approve
Jeroen T. Vermeulen (jtv) wrote :

Yes, I'll probably want to get more cleverer about keeping the profile option. Haven't quite got that finalized yet, although I have a working prototype.

I fixed the __import__.

lp:~jtv/maas/maascli-extract-parser updated on 2012-10-08
1224. By Jeroen T. Vermeulen on 2012-10-08

Review suggestion: no need for __import__.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maascli/__init__.py'
2--- src/maascli/__init__.py 2012-10-08 08:55:15 +0000
3+++ src/maascli/__init__.py 2012-10-08 16:03:32 +0000
4@@ -14,41 +14,11 @@
5 "main",
6 ]
7
8-import argparse
9-from argparse import RawDescriptionHelpFormatter
10 import locale
11 import sys
12
13 from bzrlib import osutils
14-from maascli.api import register_api_commands
15-from maascli.cli import register_cli_commands
16-from maascli.utils import parse_docstring
17-
18-
19-class ArgumentParser(argparse.ArgumentParser):
20- """Specialisation of argparse's parser with better support for subparsers.
21-
22- Specifically, the one-shot `add_subparsers` call is disabled, replaced by
23- a lazily evaluated `subparsers` property.
24- """
25-
26- def __init__(self, *args, **kwargs):
27- kwargs.setdefault("formatter_class", RawDescriptionHelpFormatter)
28- super(ArgumentParser, self).__init__(*args, **kwargs)
29-
30- def add_subparsers(self):
31- raise NotImplementedError(
32- "add_subparsers has been disabled")
33-
34- @property
35- def subparsers(self):
36- try:
37- return self.__subparsers
38- except AttributeError:
39- parent = super(ArgumentParser, self)
40- self.__subparsers = parent.add_subparsers(title="drill down")
41- self.__subparsers.metavar = "COMMAND"
42- return self.__subparsers
43+from maascli.parser import prepare_parser
44
45
46 def main(argv=None):
47@@ -58,13 +28,7 @@
48 if argv is None:
49 argv = sys.argv[:1] + osutils.get_unicode_argv()
50
51- module = __import__('maascli.api', fromlist=True)
52- help_title, help_body = parse_docstring(module)
53- parser = ArgumentParser(
54- description=help_body, prog=argv[0],
55- epilog="http://maas.ubuntu.com/")
56- register_cli_commands(parser)
57- register_api_commands(parser)
58+ parser = prepare_parser(argv)
59
60 # Run, doing polite things with exceptions.
61 try:
62
63=== added file 'src/maascli/parser.py'
64--- src/maascli/parser.py 1970-01-01 00:00:00 +0000
65+++ src/maascli/parser.py 2012-10-08 16:03:32 +0000
66@@ -0,0 +1,72 @@
67+# Copyright 2012 Canonical Ltd. This software is licensed under the
68+# GNU Affero General Public License version 3 (see the file LICENSE).
69+
70+"""Arguments parser for `maascli`."""
71+
72+from __future__ import (
73+ absolute_import,
74+ print_function,
75+ unicode_literals,
76+ )
77+
78+__metaclass__ = type
79+__all__ = [
80+ 'prepare_parser',
81+ ]
82+
83+import argparse
84+
85+from maascli import api
86+from maascli.cli import register_cli_commands
87+from maascli.utils import parse_docstring
88+
89+
90+class ArgumentParser(argparse.ArgumentParser):
91+ """Specialisation of argparse's parser with better support for subparsers.
92+
93+ Specifically, the one-shot `add_subparsers` call is disabled, replaced by
94+ a lazily evaluated `subparsers` property.
95+ """
96+
97+ def __init__(self, *args, **kwargs):
98+ kwargs.setdefault(
99+ "formatter_class", argparse.RawDescriptionHelpFormatter)
100+ super(ArgumentParser, self).__init__(*args, **kwargs)
101+
102+ def add_subparsers(self):
103+ raise NotImplementedError(
104+ "add_subparsers has been disabled")
105+
106+ @property
107+ def subparsers(self):
108+ try:
109+ return self.__subparsers
110+ except AttributeError:
111+ parent = super(ArgumentParser, self)
112+ self.__subparsers = parent.add_subparsers(title="drill down")
113+ self.__subparsers.metavar = "COMMAND"
114+ return self.__subparsers
115+
116+
117+def get_profile_option(argv):
118+ """Parse the `--profile` option in `argv`; ignore the rest."""
119+ # Create a specialized parser just to extract this one option.
120+ # If we call parse_known_args on the real arguments parser, the
121+ # --help option will do its work and cause the process to exit
122+ # before we can even add the sub-parsers that the user may be asking
123+ # for help about.
124+ specialized_parser = ArgumentParser(add_help=False)
125+ specialized_parser.add_argument('--profile', metavar='PROFILE')
126+ provisional_options = specialized_parser.parse_known_args(argv)[0]
127+ return provisional_options.profile
128+
129+
130+def prepare_parser(argv):
131+ """Create and populate an arguments parser for the maascli command."""
132+ help_title, help_body = parse_docstring(api)
133+ parser = ArgumentParser(
134+ description=help_body, prog=argv[0],
135+ epilog="http://maas.ubuntu.com/")
136+ register_cli_commands(parser)
137+ api.register_api_commands(parser)
138+ return parser
139
140=== modified file 'src/maascli/tests/test_api.py'
141--- src/maascli/tests/test_api.py 2012-10-08 13:30:28 +0000
142+++ src/maascli/tests/test_api.py 2012-10-08 16:03:32 +0000
143@@ -18,12 +18,10 @@
144 import json
145
146 import httplib2
147-from maascli import (
148- api,
149- ArgumentParser,
150- )
151+from maascli import api
152 from maascli.command import CommandError
153 from maascli.config import ProfileConfig
154+from maascli.parser import ArgumentParser
155 from maascli.testing.config import make_configs
156 from maascli.utils import (
157 handler_command_name,
158
159=== modified file 'src/maascli/tests/test_cli.py'
160--- src/maascli/tests/test_cli.py 2012-10-08 06:30:25 +0000
161+++ src/maascli/tests/test_cli.py 2012-10-08 16:03:32 +0000
162@@ -12,10 +12,8 @@
163 __metaclass__ = type
164 __all__ = []
165
166-from maascli import (
167- ArgumentParser,
168- cli,
169- )
170+from maascli import cli
171+from maascli.parser import ArgumentParser
172 from maastesting.testcase import TestCase
173
174
175
176=== renamed file 'src/maascli/tests/test_init.py' => 'src/maascli/tests/test_parser.py'
177--- src/maascli/tests/test_init.py 2012-10-05 05:12:01 +0000
178+++ src/maascli/tests/test_parser.py 2012-10-08 16:03:32 +0000
179@@ -12,11 +12,16 @@
180 __metaclass__ = type
181 __all__ = []
182
183-from maascli import ArgumentParser
184+from maascli.parser import (
185+ ArgumentParser,
186+ get_profile_option,
187+ )
188+from maastesting.factory import factory
189 from maastesting.testcase import TestCase
190
191
192 class TestArgumentParser(TestCase):
193+ """Tests for `ArgumentParser`."""
194
195 def test_add_subparsers_disabled(self):
196 parser = ArgumentParser()
197@@ -36,3 +41,29 @@
198 # The subparsers property, once populated, always returns the same
199 # object.
200 self.assertIs(subparsers, parser.subparsers)
201+
202+
203+class TestGetProfileOption(TestCase):
204+ """Tests for `get_profile_option`."""
205+
206+ def test_parses_profile_option(self):
207+ profile = factory.make_name('profile')
208+ self.assertEqual(profile, get_profile_option(['--profile', profile]))
209+
210+ def test_ignores_other_options(self):
211+ profile = factory.make_name('profile')
212+ self.assertEqual(
213+ profile,
214+ get_profile_option([
215+ '--unrelated', 'option',
216+ '--profile', profile,
217+ factory.getRandomString(),
218+ ]))
219+
220+ def test_ignores_help_option(self):
221+ # This is a bit iffy: the most likely symptom if this fails is
222+ # actually that the test process exits!
223+ profile = factory.make_name('profile')
224+ self.assertEqual(
225+ profile,
226+ get_profile_option(['--help', '--profile', profile]))