import os
import sys
from argparse import RawTextHelpFormatter, SUPPRESS
from webdriver_test_tools import config
from webdriver_test_tools.common import cmd
from webdriver_test_tools.project import new_file
[docs]def main(test_package_path, test_package, args):
"""Command line dialogs for creating a new file
This method checks ``args`` for optional arguments for each of its prompts.
If these are set to something other than ``None``, their corresponding
input prompts will be skipped unless validation for that parameter fails.
``type``, ``module_name``, and ``class_name`` are the 3 values required to
create a new file. If these are all set to something other than ``None``,
this method will default to an empty ``description`` unless one is
provided.
``force`` is the only optional parameter that does not have a prompt. It
will default to ``False`` unless the ``--force`` flag is used when calling
this method.
The ``new page`` command has additional optional arguments ``--prototype``
and ``--yaml``/``--no-yaml`` (depending on the configuration of
``ProjectFilesConfig.ENABLE_PAGE_OBJECT_YAML``). Prompt for ``--prototype``
will not be shown if ``type``, ``module_name``, and ``class_name`` are all
set to something other than ``None``. Instead, this method will use the
standard page object template unless one is specified with ``prototype``.
Currently there is no prompt for the ``--yaml``/``--no-yaml`` arguments, so
the value of ``ProjectFilesConfig.ENABLE_PAGE_OBJECT_YAML`` will always be
used unless ``--yaml``/``--no-yaml`` is specified.
:param test_package_path: The root directory of the test package
:param test_package: The python package name of the test package
:param args: Parsed arguments for the ``new`` command
"""
new_file_start = False
# Get common items from args
# (Using getattr() with default values because some attributes might not be
# present if args.type wasn't specified)
file_type = getattr(args, 'type', None)
module_name = getattr(args, 'module_name', None)
class_name = getattr(args, 'class_name', None)
description = getattr(args, 'description', None)
force = getattr(args, 'force', False)
# module and class names are the minimum required args, will ignore
# optional prompts if this is True
minimum_required_args = module_name and class_name
try:
# if module_name and class_name are set, use defaults for optional arguments
if minimum_required_args and description is None:
description = ''
_validate_file_type = cmd.validate_choice(
[new_file.TEST_TYPE, new_file.PAGE_TYPE],
shorthand_choices={'t': new_file.TEST_TYPE, 'p': new_file.PAGE_TYPE}
)
validated_file_type = cmd.prompt(
'[t]est/[p]age',
'Create a new test case or page object?',
validate=_validate_file_type,
parsed_input=file_type
)
validated_module_name = cmd.prompt(
'Module file name',
'Enter a file name for the new {} module'.format(validated_file_type),
validate=cmd.validate_module_name,
parsed_input=module_name
)
class_type = 'test case' if validated_file_type == 'test' else 'page object'
validated_class_name = cmd.prompt(
'{} class name'.format(class_type.capitalize()),
'Enter a name for the initial {} class'.format(class_type),
validate=cmd.validate_class_name,
parsed_input=class_name
)
validated_description = cmd.prompt(
'Description',
'(Optional) Enter description of the new {} class'.format(class_type),
validate=validate_description,
default='',
parsed_input=description
)
# Arguments for page-specific prompts
kwargs = {}
if validated_file_type == new_file.PAGE_TYPE:
prototype = getattr(args, 'prototype', None)
use_yaml = getattr(args, 'use_yaml', config.ProjectFilesConfig.ENABLE_PAGE_OBJECT_YAML)
if prototype is None and minimum_required_args:
prototype = ''
_prototype_choices = [name for name in new_file.PROTOTYPE_NAMES]
# Allow for numeric shorthand answers (starting at 1)
_prototype_shorthands = {
str(ind + 1): choice for ind, choice in enumerate(_prototype_choices)
}
# Allow empty string since this is an optional parameter
_prototype_choices.append('')
_validate_prototype = cmd.validate_choice(
_prototype_choices, shorthand_choices=_prototype_shorthands
)
kwargs['prototype'] = cmd.prompt(
'Page object prototype',
'(Optional) Select a page object prototype to subclass:',
*[cmd.INDENT + '[{}] {}'.format(i, name) for i, name in _prototype_shorthands.items()],
validate=_validate_prototype,
default='',
parsed_input=prototype
)
# TODO: Add prompt if class supports it (will need to change arg default to None and pass config_module as param)
kwargs['use_yaml'] = use_yaml
# Start file creation
new_file_start = True
new_file_paths = new_file.new_file(
test_package_path, test_package,
file_type=validated_file_type, module_name=validated_module_name,
class_name=validated_class_name, description=validated_description,
force=force, **kwargs
)
# Output new file path on success
# TODO: Custom success messages based on type? E.g. instructions on filling out YAML file?
success_msg = '\nFile' + ('s' if len(new_file_paths) > 1 else '') + ' created.'
print(cmd.COLORS['success'](success_msg))
for new_file_path in new_file_paths:
print(new_file_path)
except KeyboardInterrupt:
print('')
if new_file_start:
msg = 'File creation was cancelled mid-operation.'
print(cmd.COLORS['warning'](msg))
sys.exit()
# Subparser
[docs]def add_new_subparser(subparsers, config_module, formatter_class=RawTextHelpFormatter):
"""Add subparser for the ``<test_package> new`` command
:param subparsers: ``argparse._SubParsersAction`` object for the test
package ArgumentParser (i.e. the object returned by the
``add_subparsers()`` method)
:param config_module: The module object for ``<test_project>.config``
:param formatter_class: (Default: ``argparse.RawTextHelpFormatter``) Class
to use for the ``formatter_class`` parameter
:return: ``argparse.ArgumentParser`` object for the newly added ``new``
subparser
"""
if config_module is None:
config_module = config
# Get ProjectFilesConfig
project_files_config = config_module.ProjectFilesConfig if 'ProjectFilesConfig' in dir(config_module) else config.ProjectFilesConfig
# TODO: add info on no args to description or help
# Adds custom --help argument
generic_parent_parser = cmd.argparse.get_generic_parent_parser()
new_description = 'Create a new test module or page object'
new_help = new_description
new_parser = subparsers.add_parser(
'new', description=new_description, help=new_help,
parents=[generic_parent_parser],
formatter_class=formatter_class,
add_help=False, epilog=cmd.argparse.ARGPARSE_EPILOG
)
# New <type> subparsers
new_type_desc = 'Run \'{} <type> --help\' for details'.format(new_parser.prog)
new_subparsers = new_parser.add_subparsers(
title='File Types', description=new_type_desc, dest='type', metavar='<type>'
)
# New test parser
_add_new_test_subparser(formatter_class, generic_parent_parser, new_subparsers, project_files_config)
# New page object parser
_add_new_page_subparser(formatter_class, generic_parent_parser, new_subparsers, project_files_config)
return new_parser
def _add_new_test_subparser(formatter_class, generic_parent_parser, new_subparsers, project_files_config):
"""Add subparser for ``new test`` command
:param formatter_class: Class to use for the ``formatter_class`` parameter
:param generic_parent_parser: Generic parent parser to use for the
``parents`` parameter
:param new_subparsers: The subparser for the ``new`` command
:param project_files_config: The ``ProjectFilesConfig`` class
"""
new_test_parent_parser = get_new_parent_parser(
parents=[generic_parent_parser], class_name_metavar='<TestCaseClass>',
class_name_help='Name to use for the initial test case class'
)
new_test_description = 'Create a new test module'
new_test_help = new_test_description
new_subparsers.add_parser(
'test', description=new_test_description, help=new_test_help,
parents=[new_test_parent_parser],
formatter_class=formatter_class,
add_help=False, epilog=cmd.argparse.ARGPARSE_EPILOG
)
def _add_new_page_subparser(formatter_class, generic_parent_parser, new_subparsers, project_files_config):
"""Add subparser for ``new page`` command
:param formatter_class: Class to use for the ``formatter_class`` parameter
:param generic_parent_parser: Generic parent parser to use for the
``parents`` parameter
:param new_subparsers: The subparser for the ``new`` command
:param project_files_config: The ``ProjectFilesConfig`` class
"""
new_page_parent_parser = get_new_parent_parser(
parents=[generic_parent_parser], class_name_metavar='<PageObjectClass>',
class_name_help='Name to use for the initial page object class'
)
new_page_description = 'Create a new page object module'
new_page_help = new_page_description
new_page_parser = new_subparsers.add_parser(
'page', description=new_page_description, help=new_page_help,
parents=[new_page_parent_parser],
formatter_class=formatter_class,
add_help=False, epilog=cmd.argparse.ARGPARSE_EPILOG
)
prototype_group = new_page_parser.add_argument_group('Prototype Options')
prototype_options_help = _format_prototype_choices()
prototype_help = 'Page object prototype to subclass.' + prototype_options_help
prototype_group.add_argument('-p', '--prototype', metavar='<prototype_choice>', default=None,
choices=new_file.PROTOTYPE_NAMES, help=prototype_help)
yaml_default = project_files_config.ENABLE_PAGE_OBJECT_YAML
# Add options to negate YAML default config
if yaml_default:
no_yaml_help = 'Only generate .py files if using a --prototype that supports YAML parsing' + _format_yaml_prototype_choices()
yaml_help = SUPPRESS
else:
yaml_help = 'Generate .py and .yml files if using a --prototype that supports YAML parsing' + _format_yaml_prototype_choices()
no_yaml_help = SUPPRESS
yaml_group = prototype_group.add_mutually_exclusive_group()
yaml_group.add_argument('--yaml', '-y', action='store_true', default=yaml_default,
dest='use_yaml', help=yaml_help)
yaml_group.add_argument('--no-yaml', '-Y', action='store_false', default=yaml_default,
dest='use_yaml', help=no_yaml_help)
def _format_prototype_choices():
"""Format the help string for page object prototype choices
The returned string will have the following format:
.. code:: python
'\\nOptions: {prototype,"prototype with spaces"}'
:return: Formatted help string for prototype options
"""
# Add quotes around names with spaces
formatted_prototype_names = [
'"{}"'.format(name) if ' ' in name else name
for name in new_file.PROTOTYPE_NAMES
]
return '\nOptions: {{{}}}'.format(','.join(formatted_prototype_names))
def _format_yaml_prototype_choices():
"""Format the help string for prototype choices with YAML support
The returned string will have the following format:
.. code:: python
'\\nSupported Prototypes: {prototype,"prototype with spaces"}'
:return: Formatted help string for prototype options
"""
# Add quotes around names with spaces
formatted_prototype_names = [
'"{}"'.format(name) if ' ' in name else name
for name in new_file.YAML_PROTOTYPE_NAMES
]
return '\nSupported Prototypes: {{{}}}'.format(','.join(formatted_prototype_names))
[docs]def get_new_parent_parser(parents=[], class_name_metavar='<ClassName>',
class_name_help='Name to use for the initial class'):
"""Returns an :class:`ArgumentParser
<webdriver_test_tools.cmd.argparse.ArgumentParser>` with ``<module_name>``,
``<class_name>``, and ``--description`` arguments
:param parents: (Optional) List of ``ArgumentParser`` objects to use as
parents for the test argument parser
:param class_name_metavar: (Optional) Metavar to display for the class_name
argument
:param class_name_help: (Optional) Help text to use for the class_name
argument
:return: :class:`ArgumentParser
<webdriver_test_tools.cmd.argparse.ArgumentParser>` with
``<module_name>``, ``<class_name>``, and ``--description`` arguments
"""
new_parent_parser = cmd.argparse.ArgumentParser(add_help=False, parents=parents)
positional_args_group = new_parent_parser.add_argument_group('Positional Arguments')
# Positional arguments
module_name_help = 'Filename to use for the new python module'
positional_args_group.add_argument('module_name', metavar='<module_name>', nargs='?', default=None,
help=module_name_help)
positional_args_group.add_argument('class_name', metavar=class_name_metavar, nargs='?', default=None,
help=class_name_help)
# Optional arguments
optional_args_group = new_parent_parser.add_argument_group('Optional Arguments')
description_help='Description for the initial class'
optional_args_group.add_argument('-d', '--description', metavar='<description>', default=None,
help=description_help)
force_help='Force overwrite if a file with the same name already exists'
optional_args_group.add_argument('-f', '--force', action='store_true', default=False,
help=force_help)
return new_parent_parser
# Argument parsing functions
# TODO: DOC exceptions that may be raised
[docs]def parse_new_args(package_name, tests_module, args):
"""Parse arguments and run the 'new' command
:param package_name: Name of the test package
:param tests_module: The module object for ``<test_project>.tests``. Used
to determine the filepath of the package
:param args: The namespace returned by parser.parse_args()
:return: Exit code, 0 if files were created without exceptions, 1 otherwise.
.. note::
Technically, this will always return 0, as all fail states cause an
exception to be raised. This is just to keep it consistent with
other project cmd parse arg functions.
"""
exit_code = 0
# Get package path based on tests_module path
test_package_path = os.path.dirname(os.path.dirname(tests_module.__file__))
main(test_package_path, package_name, args)
return exit_code
# User Input Prompts
[docs]def validate_description(description):
"""Replaces double quotes with single quotes in class description
If the description is ``None`` or an empty string, this function considers
it valid and returns ``None``
:param description: The desired description string
:return: Validated description string with double quotes replaced with
single quotes or ``None`` if the description is empty
"""
if description is None or description == '':
return None
# Replace double quotes with single quotes to avoid breaking the docstring
validated_description = description.replace('"', "'")
if validated_description != description:
cmd.print_info('Replaced double quotes with single quotes in class description')
return validated_description