blob: de9dd13fe94b7986d3500501de3f8694e7bfe358 [file] [log] [blame]
Tarek Ziade1231a4e2011-05-19 13:07:25 +02001"""Main command line parser. Implements the pysetup script."""
2
3import os
4import re
5import sys
6import getopt
7import logging
Tarek Ziadeb1b6e132011-05-30 12:07:49 +02008from copy import copy
Tarek Ziade1231a4e2011-05-19 13:07:25 +02009
10from packaging import logger
11from packaging.dist import Distribution
Tarek Ziade721ccd02011-06-02 12:00:44 +020012from packaging.util import _is_archive_file, generate_setup_py
Tarek Ziade1231a4e2011-05-19 13:07:25 +020013from packaging.command import get_command_class, STANDARD_COMMANDS
14from packaging.install import install, install_local_project, remove
15from packaging.database import get_distribution, get_distributions
16from packaging.depgraph import generate_graph
17from packaging.fancy_getopt import FancyGetopt
18from packaging.errors import (PackagingArgError, PackagingError,
19 PackagingModuleError, PackagingClassError,
20 CCompilerError)
21
22
23command_re = re.compile(r'^[a-zA-Z]([a-zA-Z0-9_]*)$')
24
25common_usage = """\
26Actions:
27%(actions)s
28
29To get more help on an action, use:
30
31 pysetup action --help
32"""
33
34create_usage = """\
35Usage: pysetup create
36 or: pysetup create --help
37
38Create a new Python package.
39"""
40
Tarek Ziade721ccd02011-06-02 12:00:44 +020041generate_usage = """\
42Usage: pysetup generate-setup
43 or: pysetup generate-setup --help
44
45Generates a setup.py script for backward-compatibility purposes.
46"""
47
48
Tarek Ziade1231a4e2011-05-19 13:07:25 +020049graph_usage = """\
50Usage: pysetup graph dist
51 or: pysetup graph --help
52
53Print dependency graph for the distribution.
54
55positional arguments:
56 dist installed distribution name
57"""
58
59install_usage = """\
60Usage: pysetup install [dist]
61 or: pysetup install [archive]
62 or: pysetup install [src_dir]
63 or: pysetup install --help
64
65Install a Python distribution from the indexes, source directory, or sdist.
66
67positional arguments:
68 archive path to source distribution (zip, tar.gz)
69 dist distribution name to install from the indexes
70 scr_dir path to source directory
71
72"""
73
74metadata_usage = """\
75Usage: pysetup metadata [dist] [-f field ...]
76 or: pysetup metadata [dist] [--all]
77 or: pysetup metadata --help
78
79Print metadata for the distribution.
80
81positional arguments:
82 dist installed distribution name
83
84optional arguments:
85 -f metadata field to print
86 --all print all metadata fields
87"""
88
89remove_usage = """\
90Usage: pysetup remove dist [-y]
91 or: pysetup remove --help
92
93Uninstall a Python distribution.
94
95positional arguments:
96 dist installed distribution name
97
98optional arguments:
99 -y auto confirm package removal
100"""
101
102run_usage = """\
103Usage: pysetup run [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
104 or: pysetup run --help
105 or: pysetup run --list-commands
106 or: pysetup run cmd --help
107"""
108
109list_usage = """\
110Usage: pysetup list dist [dist ...]
111 or: pysetup list --help
112 or: pysetup list --all
113
114Print name, version and location for the matching installed distributions.
115
116positional arguments:
117 dist installed distribution name
118
119optional arguments:
120 --all list all installed distributions
121"""
122
123search_usage = """\
124Usage: pysetup search [project] [--simple [url]] [--xmlrpc [url] [--fieldname value ...] --operator or|and]
125 or: pysetup search --help
126
127Search the indexes for the matching projects.
128
129positional arguments:
130 project the project pattern to search for
131
132optional arguments:
133 --xmlrpc [url] wether to use the xmlrpc index or not. If an url is
134 specified, it will be used rather than the default one.
135
136 --simple [url] wether to use the simple index or not. If an url is
137 specified, it will be used rather than the default one.
138
139 --fieldname value Make a search on this field. Can only be used if
140 --xmlrpc has been selected or is the default index.
141
142 --operator or|and Defines what is the operator to use when doing xmlrpc
143 searchs with multiple fieldnames. Can only be used if
144 --xmlrpc has been selected or is the default index.
145"""
146
147global_options = [
148 # The fourth entry for verbose means that it can be repeated.
149 ('verbose', 'v', "run verbosely (default)", True),
150 ('quiet', 'q', "run quietly (turns verbosity off)"),
151 ('dry-run', 'n', "don't actually do anything"),
152 ('help', 'h', "show detailed help message"),
153 ('no-user-cfg', None, 'ignore pydistutils.cfg in your home directory'),
154 ('version', None, 'Display the version'),
155]
156
157negative_opt = {'quiet': 'verbose'}
158
159display_options = [
160 ('help-commands', None, "list all available commands"),
161]
162
163display_option_names = [x[0].replace('-', '_') for x in display_options]
164
165
166def _parse_args(args, options, long_options):
167 """Transform sys.argv input into a dict.
168
169 :param args: the args to parse (i.e sys.argv)
170 :param options: the list of options to pass to getopt
171 :param long_options: the list of string with the names of the long options
172 to be passed to getopt.
173
174 The function returns a dict with options/long_options as keys and matching
175 values as values.
176 """
177 optlist, args = getopt.gnu_getopt(args, options, long_options)
178 optdict = {}
179 optdict['args'] = args
180 for k, v in optlist:
181 k = k.lstrip('-')
182 if k not in optdict:
183 optdict[k] = []
184 if v:
185 optdict[k].append(v)
186 else:
187 optdict[k].append(v)
188 return optdict
189
190
191class action_help:
192 """Prints a help message when the standard help flags: -h and --help
193 are used on the commandline.
194 """
195
196 def __init__(self, help_msg):
197 self.help_msg = help_msg
198
199 def __call__(self, f):
200 def wrapper(*args, **kwargs):
201 f_args = args[1]
202 if '--help' in f_args or '-h' in f_args:
203 print(self.help_msg)
204 return
205 return f(*args, **kwargs)
206 return wrapper
207
208
209@action_help(create_usage)
210def _create(distpatcher, args, **kw):
211 from packaging.create import main
212 return main()
213
214
Tarek Ziade721ccd02011-06-02 12:00:44 +0200215@action_help(generate_usage)
216def _generate(distpatcher, args, **kw):
217 generate_setup_py()
218 print('The setup.py was generated')
219
220
221
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200222@action_help(graph_usage)
223def _graph(dispatcher, args, **kw):
224 name = args[1]
225 dist = get_distribution(name, use_egg_info=True)
226 if dist is None:
227 print('Distribution not found.')
228 else:
229 dists = get_distributions(use_egg_info=True)
230 graph = generate_graph(dists)
231 print(graph.repr_node(dist))
232
233
234@action_help(install_usage)
235def _install(dispatcher, args, **kw):
236 # first check if we are in a source directory
237 if len(args) < 2:
238 # are we inside a project dir?
239 listing = os.listdir(os.getcwd())
240 if 'setup.py' in listing or 'setup.cfg' in listing:
241 args.insert(1, os.getcwd())
242 else:
Tarek Ziade5a5ce382011-05-31 12:09:34 +0200243 logger.warning('No project to install.')
244 return 1
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200245
Tarek Ziadeb1b6e132011-05-30 12:07:49 +0200246 target = args[1]
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200247 # installing from a source dir or archive file?
Tarek Ziadeb1b6e132011-05-30 12:07:49 +0200248 if os.path.isdir(target) or _is_archive_file(target):
Tarek Ziade5a5ce382011-05-31 12:09:34 +0200249 if install_local_project(target):
250 return 0
251 else:
252 return 1
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200253 else:
254 # download from PyPI
Tarek Ziade5a5ce382011-05-31 12:09:34 +0200255 if install(target):
256 return 0
257 else:
258 return 1
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200259
260
261@action_help(metadata_usage)
262def _metadata(dispatcher, args, **kw):
263 opts = _parse_args(args[1:], 'f:', ['all'])
264 if opts['args']:
265 name = opts['args'][0]
266 dist = get_distribution(name, use_egg_info=True)
267 if dist is None:
268 logger.warning('%s not installed', name)
269 return
270 else:
271 logger.info('searching local dir for metadata')
272 dist = Distribution()
273 dist.parse_config_files()
274
275 metadata = dist.metadata
276
277 if 'all' in opts:
278 keys = metadata.keys()
279 else:
280 if 'f' in opts:
281 keys = (k for k in opts['f'] if k in metadata)
282 else:
283 keys = ()
284
285 for key in keys:
286 if key in metadata:
287 print(metadata._convert_name(key) + ':')
288 value = metadata[key]
289 if isinstance(value, list):
290 for v in value:
291 print(' ' + v)
292 else:
293 print(' ' + value.replace('\n', '\n '))
294
295
296@action_help(remove_usage)
297def _remove(distpatcher, args, **kw):
298 opts = _parse_args(args[1:], 'y', [])
299 if 'y' in opts:
300 auto_confirm = True
301 else:
302 auto_confirm = False
303
304 for dist in set(opts['args']):
305 try:
306 remove(dist, auto_confirm=auto_confirm)
307 except PackagingError:
308 logger.warning('%s not installed', dist)
309
310
311@action_help(run_usage)
312def _run(dispatcher, args, **kw):
313 parser = dispatcher.parser
314 args = args[1:]
315
316 commands = STANDARD_COMMANDS # + extra commands
317
318 if args == ['--list-commands']:
319 print('List of available commands:')
320 cmds = sorted(commands)
321
322 for cmd in cmds:
323 cls = dispatcher.cmdclass.get(cmd) or get_command_class(cmd)
324 desc = getattr(cls, 'description',
325 '(no description available)')
326 print(' %s: %s' % (cmd, desc))
327 return
328
329 while args:
330 args = dispatcher._parse_command_opts(parser, args)
331 if args is None:
332 return
333
334 # create the Distribution class
335 # need to feed setup.cfg here !
336 dist = Distribution()
337
338 # Find and parse the config file(s): they will override options from
339 # the setup script, but be overridden by the command line.
340
341 # XXX still need to be extracted from Distribution
342 dist.parse_config_files()
343
344 try:
345 for cmd in dispatcher.commands:
346 dist.run_command(cmd, dispatcher.command_options[cmd])
347
348 except KeyboardInterrupt:
349 raise SystemExit("interrupted")
350 except (IOError, os.error, PackagingError, CCompilerError) as msg:
351 raise SystemExit("error: " + str(msg))
352
353 # XXX this is crappy
354 return dist
355
356
357@action_help(list_usage)
358def _list(dispatcher, args, **kw):
359 opts = _parse_args(args[1:], '', ['all'])
360 dists = get_distributions(use_egg_info=True)
Tarek Ziade441531f2011-05-31 09:18:24 +0200361 if 'all' in opts or opts['args'] == []:
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200362 results = dists
363 else:
364 results = [d for d in dists if d.name.lower() in opts['args']]
365
Tarek Ziade441531f2011-05-31 09:18:24 +0200366 number = 0
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200367 for dist in results:
368 print('%s %s at %s' % (dist.name, dist.metadata['version'], dist.path))
Tarek Ziadee2655972011-05-31 12:15:42 +0200369 number += 1
Tarek Ziade441531f2011-05-31 09:18:24 +0200370
371 print('')
372 if number == 0:
373 print('Nothing seems to be installed.')
374 else:
375 print('Found %d projects installed.' % number)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200376
377
378@action_help(search_usage)
379def _search(dispatcher, args, **kw):
380 """The search action.
381
382 It is able to search for a specific index (specified with --index), using
383 the simple or xmlrpc index types (with --type xmlrpc / --type simple)
384 """
Tarek Ziadee2655972011-05-31 12:15:42 +0200385 #opts = _parse_args(args[1:], '', ['simple', 'xmlrpc'])
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200386 # 1. what kind of index is requested ? (xmlrpc / simple)
Tarek Ziadee2655972011-05-31 12:15:42 +0200387 raise NotImplementedError()
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200388
389
390actions = [
391 ('run', 'Run one or several commands', _run),
392 ('metadata', 'Display the metadata of a project', _metadata),
393 ('install', 'Install a project', _install),
394 ('remove', 'Remove a project', _remove),
395 ('search', 'Search for a project in the indexes', _search),
396 ('list', 'Search for local projects', _list),
397 ('graph', 'Display a graph', _graph),
398 ('create', 'Create a Project', _create),
Tarek Ziade721ccd02011-06-02 12:00:44 +0200399 ('generate-setup', 'Generates a backward-comptatible setup.py', _generate)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200400]
401
402
403class Dispatcher:
404 """Reads the command-line options
405 """
406 def __init__(self, args=None):
407 self.verbose = 1
408 self.dry_run = False
409 self.help = False
410 self.script_name = 'pysetup'
411 self.cmdclass = {}
412 self.commands = []
413 self.command_options = {}
414
415 for attr in display_option_names:
416 setattr(self, attr, False)
417
418 self.parser = FancyGetopt(global_options + display_options)
419 self.parser.set_negative_aliases(negative_opt)
420 # FIXME this parses everything, including command options (e.g. "run
421 # build -i" errors with "option -i not recognized")
422 args = self.parser.getopt(args=args, object=self)
423
424 # if first arg is "run", we have some commands
425 if len(args) == 0:
426 self.action = None
427 else:
428 self.action = args[0]
429
430 allowed = [action[0] for action in actions] + [None]
431 if self.action not in allowed:
432 msg = 'Unrecognized action "%s"' % self.action
433 raise PackagingArgError(msg)
434
Tarek Ziadeb1b6e132011-05-30 12:07:49 +0200435 self._set_logger()
Tarek Ziadeb1b6e132011-05-30 12:07:49 +0200436 self.args = args
437
Tarek Ziadee2655972011-05-31 12:15:42 +0200438 # for display options we return immediately
Tarek Ziadeb1b6e132011-05-30 12:07:49 +0200439 if self.help or self.action is None:
440 self._show_help(self.parser, display_options_=False)
441
442 def _set_logger(self):
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200443 # setting up the logging level from the command-line options
444 # -q gets warning, error and critical
445 if self.verbose == 0:
446 level = logging.WARNING
447 # default level or -v gets info too
448 # XXX there's a bug somewhere: the help text says that -v is default
449 # (and verbose is set to 1 above), but when the user explicitly gives
450 # -v on the command line, self.verbose is incremented to 2! Here we
451 # compensate for that (I tested manually). On a related note, I think
452 # it's a good thing to use -q/nothing/-v/-vv on the command line
453 # instead of logging constants; it will be easy to add support for
454 # logging configuration in setup.cfg for advanced users. --merwok
455 elif self.verbose in (1, 2):
456 level = logging.INFO
457 else: # -vv and more for debug
458 level = logging.DEBUG
459
Tarek Ziadeb1b6e132011-05-30 12:07:49 +0200460 # setting up the stream handler
461 handler = logging.StreamHandler(sys.stderr)
462 handler.setLevel(level)
463 logger.addHandler(handler)
464 logger.setLevel(level)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200465
466 def _parse_command_opts(self, parser, args):
467 # Pull the current command from the head of the command line
468 command = args[0]
469 if not command_re.match(command):
470 raise SystemExit("invalid command name %r" % (command,))
471 self.commands.append(command)
472
473 # Dig up the command class that implements this command, so we
474 # 1) know that it's a valid command, and 2) know which options
475 # it takes.
476 try:
477 cmd_class = get_command_class(command)
478 except PackagingModuleError as msg:
479 raise PackagingArgError(msg)
480
481 # XXX We want to push this in packaging.command
482 #
483 # Require that the command class be derived from Command -- want
484 # to be sure that the basic "command" interface is implemented.
485 for meth in ('initialize_options', 'finalize_options', 'run'):
486 if hasattr(cmd_class, meth):
487 continue
488 raise PackagingClassError(
489 'command %r must implement %r' % (cmd_class, meth))
490
491 # Also make sure that the command object provides a list of its
492 # known options.
493 if not (hasattr(cmd_class, 'user_options') and
494 isinstance(cmd_class.user_options, list)):
495 raise PackagingClassError(
496 "command class %s must provide "
497 "'user_options' attribute (a list of tuples)" % cmd_class)
498
499 # If the command class has a list of negative alias options,
500 # merge it in with the global negative aliases.
501 _negative_opt = negative_opt.copy()
502
503 if hasattr(cmd_class, 'negative_opt'):
504 _negative_opt.update(cmd_class.negative_opt)
505
506 # Check for help_options in command class. They have a different
507 # format (tuple of four) so we need to preprocess them here.
508 if (hasattr(cmd_class, 'help_options') and
509 isinstance(cmd_class.help_options, list)):
510 help_options = cmd_class.help_options[:]
511 else:
512 help_options = []
513
514 # All commands support the global options too, just by adding
515 # in 'global_options'.
516 parser.set_option_table(global_options +
517 cmd_class.user_options +
518 help_options)
519 parser.set_negative_aliases(_negative_opt)
520 args, opts = parser.getopt(args[1:])
521
522 if hasattr(opts, 'help') and opts.help:
523 self._show_command_help(cmd_class)
524 return
525
526 if (hasattr(cmd_class, 'help_options') and
527 isinstance(cmd_class.help_options, list)):
528 help_option_found = False
529 for help_option, short, desc, func in cmd_class.help_options:
530 if hasattr(opts, help_option.replace('-', '_')):
531 help_option_found = True
532 if hasattr(func, '__call__'):
533 func()
534 else:
535 raise PackagingClassError(
536 "invalid help function %r for help option %r: "
537 "must be a callable object (function, etc.)"
538 % (func, help_option))
539
540 if help_option_found:
541 return
542
543 # Put the options from the command line into their official
544 # holding pen, the 'command_options' dictionary.
545 opt_dict = self.get_option_dict(command)
546 for name, value in vars(opts).items():
547 opt_dict[name] = ("command line", value)
548
549 return args
550
551 def get_option_dict(self, command):
552 """Get the option dictionary for a given command. If that
553 command's option dictionary hasn't been created yet, then create it
554 and return the new dictionary; otherwise, return the existing
555 option dictionary.
556 """
557 d = self.command_options.get(command)
558 if d is None:
559 d = self.command_options[command] = {}
560 return d
561
562 def show_help(self):
563 self._show_help(self.parser)
564
565 def print_usage(self, parser):
566 parser.set_option_table(global_options)
567
568 actions_ = [' %s: %s' % (name, desc) for name, desc, __ in actions]
569 usage = common_usage % {'actions': '\n'.join(actions_)}
570
571 parser.print_help(usage + "\nGlobal options:")
572
573 def _show_help(self, parser, global_options_=True, display_options_=True,
574 commands=[]):
575 # late import because of mutual dependence between these modules
576 from packaging.command.cmd import Command
577
578 print('Usage: pysetup [options] action [action_options]')
579 print('')
580 if global_options_:
581 self.print_usage(self.parser)
582 print('')
583
584 if display_options_:
585 parser.set_option_table(display_options)
586 parser.print_help(
587 "Information display options (just display " +
588 "information, ignore any commands)")
589 print('')
590
591 for command in commands:
592 if isinstance(command, type) and issubclass(command, Command):
593 cls = command
594 else:
595 cls = get_command_class(command)
596 if (hasattr(cls, 'help_options') and
597 isinstance(cls.help_options, list)):
598 parser.set_option_table(cls.user_options + cls.help_options)
599 else:
600 parser.set_option_table(cls.user_options)
601
602 parser.print_help("Options for %r command:" % cls.__name__)
603 print('')
604
605 def _show_command_help(self, command):
606 if isinstance(command, str):
607 command = get_command_class(command)
608
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200609 desc = getattr(command, 'description', '(no description available)')
610 print('Description: %s' % desc)
611 print('')
612
613 if (hasattr(command, 'help_options') and
614 isinstance(command.help_options, list)):
615 self.parser.set_option_table(command.user_options +
616 command.help_options)
617 else:
618 self.parser.set_option_table(command.user_options)
619
620 self.parser.print_help("Options:")
621 print('')
622
623 def _get_command_groups(self):
624 """Helper function to retrieve all the command class names divided
625 into standard commands (listed in
626 packaging.command.STANDARD_COMMANDS) and extra commands (given in
627 self.cmdclass and not standard commands).
628 """
629 extra_commands = [cmd for cmd in self.cmdclass
630 if cmd not in STANDARD_COMMANDS]
631 return STANDARD_COMMANDS, extra_commands
632
633 def print_commands(self):
634 """Print out a help message listing all available commands with a
635 description of each. The list is divided into standard commands
636 (listed in packaging.command.STANDARD_COMMANDS) and extra commands
637 (given in self.cmdclass and not standard commands). The
638 descriptions come from the command class attribute
639 'description'.
640 """
641 std_commands, extra_commands = self._get_command_groups()
642 max_length = max(len(command)
643 for commands in (std_commands, extra_commands)
644 for command in commands)
645
646 self.print_command_list(std_commands, "Standard commands", max_length)
647 if extra_commands:
648 print()
649 self.print_command_list(extra_commands, "Extra commands",
650 max_length)
651
652 def print_command_list(self, commands, header, max_length):
653 """Print a subset of the list of all commands -- used by
654 'print_commands()'.
655 """
656 print(header + ":")
657
658 for cmd in commands:
659 cls = self.cmdclass.get(cmd) or get_command_class(cmd)
660 description = getattr(cls, 'description',
661 '(no description available)')
662
663 print(" %-*s %s" % (max_length, cmd, description))
664
665 def __call__(self):
666 if self.action is None:
667 return
668 for action, desc, func in actions:
669 if action == self.action:
670 return func(self, self.args)
671 return -1
672
673
674def main(args=None):
Tarek Ziadeb1b6e132011-05-30 12:07:49 +0200675 old_level = logger.level
676 old_handlers = copy(logger.handlers)
677 try:
678 dispatcher = Dispatcher(args)
679 if dispatcher.action is None:
680 return
681 return dispatcher()
682 finally:
683 logger.setLevel(old_level)
684 logger.handlers[:] = old_handlers
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200685
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200686
687if __name__ == '__main__':
688 sys.exit(main())