class Configuration(object): """Configuration object for behave and behave runners.""" # pylint: disable=too-many-instance-attributes defaults = dict( color=sys.platform != "win32", show_snippets=True, show_skipped=True, dry_run=False, show_source=True, show_timings=True, stdout_capture=True, stderr_capture=True, log_capture=True, logging_format="%(levelname)s:%(name)s:%(message)s", logging_level=logging.INFO, steps_catalog=False, summary=True, junit=False, stage=None, userdata={}, # -- SPECIAL: default_format="pretty", # -- Used when no formatters are configured. default_tags="", # -- Used when no tags are defined. scenario_outline_annotation_schema=u"{name} -- @{row.id} {examples.name}" ) cmdline_only_options = set("userdata_defines") def __init__(self, command_args=None, load_config=True, verbose=None, **kwargs): """ Constructs a behave configuration object. * loads the configuration defaults (if needed). * process the command-line args * store the configuration results :param command_args: Provide command args (as sys.argv). If command_args is None, sys.argv[1:] is used. :type command_args: list<str>, str :param load_config: Indicate if configfile should be loaded (=true) :param verbose: Indicate if diagnostic output is enabled :param kwargs: Used to hand-over/overwrite default values. """ # pylint: disable=too-many-branches, too-many-statements if command_args is None: command_args = sys.argv[1:] elif isinstance(command_args, six.string_types): if six.PY2 and isinstance(command_args, six.text_type): command_args = command_args.encode("utf-8") elif six.PY3 and isinstance(command_args, six.binary_type): command_args = command_args.decode("utf-8") command_args = shlex.split(command_args) if verbose is None: # -- AUTO-DISCOVER: Verbose mode from command-line args. verbose = ("-v" in command_args) or ("--verbose" in command_args) self.version = None self.tags_help = None self.lang_list = None self.lang_help = None self.default_tags = None self.junit = None self.logging_format = None self.logging_datefmt = None self.name = None self.scope = None self.steps_catalog = None self.userdata = None self.wip = None defaults = self.defaults.copy() for name, value in six.iteritems(kwargs): defaults[name] = value self.defaults = defaults self.formatters = [] self.reporters = [] self.name_re = None self.outputs = [] self.include_re = None self.exclude_re = None self.scenario_outline_annotation_schema = None # pylint: disable=invalid-name self.steps_dir = "steps" self.environment_file = "environment.py" self.userdata_defines = None self.more_formatters = None if load_config: load_configuration(self.defaults, verbose=verbose) parser = setup_parser() parser.set_defaults(**self.defaults) args = parser.parse_args(command_args) for key, value in six.iteritems(args.__dict__): if key.startswith("_") and key not in self.cmdline_only_options: continue setattr(self, key, value) self.paths = [os.path.normpath(path) for path in self.paths] self.setup_outputs(args.outfiles) if self.steps_catalog: # -- SHOW STEP-CATALOG: As step summary. self.default_format = "steps.catalog" self.format = ["steps.catalog"] self.dry_run = True self.summary = False self.show_skipped = False self.quiet = True if self.wip: # Only run scenarios tagged with "wip". # Additionally: # * use the "plain" formatter (per default) # * do not capture stdout or logging output and # * stop at the first failure. self.default_format = "plain" self.tags = ["wip"] + self.default_tags.split() self.color = False self.stop = True self.log_capture = False self.stdout_capture = False self.tags = TagExpression(self.tags or self.default_tags.split()) if self.quiet: self.show_source = False self.show_snippets = False if self.exclude_re: self.exclude_re = re.compile(self.exclude_re) if self.include_re: self.include_re = re.compile(self.include_re) if self.name: # -- SELECT: Scenario-by-name, build regular expression. self.name_re = self.build_name_re(self.name) if self.stage is None: # pylint: disable=access-member-before-definition # -- USE ENVIRONMENT-VARIABLE, if stage is undefined. self.stage = os.environ.get("BEHAVE_STAGE", None) self.setup_stage(self.stage) self.setup_model() self.setup_userdata() # -- FINALLY: Setup Reporters and Formatters # NOTE: Reporters and Formatters can now use userdata information. if self.junit: # Buffer the output (it will be put into Junit report) self.stdout_capture = True self.stderr_capture = True self.log_capture = True self.reporters.append(JUnitReporter(self)) if self.summary: self.reporters.append(SummaryReporter(self)) self.setup_formats() unknown_formats = self.collect_unknown_formats() if unknown_formats: parser.error("format=%s is unknown" % ", ".join(unknown_formats)) def setup_outputs(self, args_outfiles=None): if self.outputs: assert not args_outfiles, "ONLY-ONCE" return # -- NORMAL CASE: Setup only initially (once). if not args_outfiles: self.outputs.append(StreamOpener(stream=sys.stdout)) else: for outfile in args_outfiles: if outfile and outfile != "-": self.outputs.append(StreamOpener(outfile)) else: self.outputs.append(StreamOpener(stream=sys.stdout)) def setup_formats(self): """Register more, user-defined formatters by name.""" if self.more_formatters: for name, scoped_class_name in self.more_formatters.items(): _format_registry.register_as(name, scoped_class_name) def collect_unknown_formats(self): unknown_formats = [] if self.format: for format_name in self.format: if (format_name == "help" or _format_registry.is_formatter_valid(format_name)): continue unknown_formats.append(format_name) return unknown_formats @staticmethod def build_name_re(names): """ Build regular expression for scenario selection by name by using a list of name parts or name regular expressions. :param names: List of name parts or regular expressions (as text). :return: Compiled regular expression to use. """ # -- NOTE: re.LOCALE is removed in Python 3.6 (deprecated in Python 3.5) # flags = (re.UNICODE | re.LOCALE) pattern = u"|".join(names) return re.compile(pattern, flags=re.UNICODE) def exclude(self, filename): if isinstance(filename, FileLocation): filename = six.text_type(filename) if self.include_re and self.include_re.search(filename) is None: return True if self.exclude_re and self.exclude_re.search(filename) is not None: return True return False def setup_logging(self, level=None, configfile=None, **kwargs): """ Support simple setup of logging subsystem. Ensures that the logging level is set. But note that the logging setup can only occur once. SETUP MODES: * :func:`logging.config.fileConfig()`, if ``configfile`` is provided. * :func:`logging.basicConfig()`, otherwise. .. code-block: python # -- FILE: features/environment.py def before_all(context): context.config.setup_logging() :param level: Logging level of root logger. If None, use :attr:`logging_level` value. :param configfile: Configuration filename for fileConfig() setup. :param kwargs: Passed to :func:`logging.basicConfig()` """ if level is None: level = self.logging_level # pylint: disable=no-member if configfile: from logging.config import fileConfig fileConfig(configfile) else: # pylint: disable=no-member format_ = kwargs.pop("format", self.logging_format) datefmt = kwargs.pop("datefmt", self.logging_datefmt) logging.basicConfig(format=format_, datefmt=datefmt, **kwargs) # -- ENSURE: Default log level is set # (even if logging subsystem is already configured). logging.getLogger().setLevel(level) def setup_model(self): if self.scenario_outline_annotation_schema: name_schema = six.text_type(self.scenario_outline_annotation_schema) ScenarioOutline.annotation_schema = name_schema.strip() def setup_stage(self, stage=None): """Setup the test stage that selects a different set of steps and environment implementations. :param stage: Name of current test stage (as string or None). EXAMPLE:: # -- SETUP DEFAULT TEST STAGE (unnamed): config = Configuration() config.setup_stage() assert config.steps_dir == "steps" assert config.environment_file == "environment.py" # -- SETUP PRODUCT TEST STAGE: config.setup_stage("product") assert config.steps_dir == "product_steps" assert config.environment_file == "product_environment.py" """ steps_dir = "steps" environment_file = "environment.py" if stage: # -- USE A TEST STAGE: Select different set of implementations. prefix = stage + "_" steps_dir = prefix + steps_dir environment_file = prefix + environment_file self.steps_dir = steps_dir self.environment_file = environment_file def setup_userdata(self): if not isinstance(self.userdata, UserData): self.userdata = UserData(self.userdata) if self.userdata_defines: # -- ENSURE: Cmd-line overrides configuration file parameters. self.userdata.update(self.userdata_defines) def update_userdata(self, data): """Update userdata with data and reapply userdata defines (cmdline). :param data: Provides (partial) userdata (as dict) """ self.userdata.update(data) if self.userdata_defines: # -- REAPPLY: Cmd-line defines (override configuration file data). self.userdata.update(self.userdata_defines)
class Configuration(object): """Configuration object for behave and behave runners.""" # pylint: disable=too-many-instance-attributes defaults = dict( color=sys.platform != "win32", show_snippets=True, show_skipped=True, dry_run=False, show_source=True, show_timings=True, stdout_capture=True, stderr_capture=True, log_capture=True, logging_format="%(levelname)s:%(name)s:%(message)s", logging_level=logging.INFO, steps_catalog=False, summary=True, junit=False, stage=None, userdata={}, # -- SPECIAL: default_format="pretty", # -- Used when no formatters are configured. default_tags="", # -- Used when no tags are defined. scenario_outline_annotation_schema= u"{name} -- @{row.id} {examples.name}") cmdline_only_options = set("userdata_defines") def __init__(self, command_args=None, load_config=True, verbose=None, **kwargs): """ Constructs a behave configuration object. * loads the configuration defaults (if needed). * process the command-line args * store the configuration results :param command_args: Provide command args (as sys.argv). If command_args is None, sys.argv[1:] is used. :type command_args: list<str>, str :param load_config: Indicate if configfile should be loaded (=true) :param verbose: Indicate if diagnostic output is enabled :param kwargs: Used to hand-over/overwrite default values. """ # pylint: disable=too-many-branches, too-many-statements if command_args is None: command_args = sys.argv[1:] elif isinstance(command_args, six.string_types): encoding = select_best_encoding() or "utf-8" if six.PY2 and isinstance(command_args, six.text_type): command_args = command_args.encode(encoding) elif six.PY3 and isinstance(command_args, six.binary_type): command_args = command_args.decode(encoding) command_args = shlex.split(command_args) elif isinstance(command_args, (list, tuple)): command_args = to_texts(command_args) if verbose is None: # -- AUTO-DISCOVER: Verbose mode from command-line args. verbose = ("-v" in command_args) or ("--verbose" in command_args) self.version = None self.tags_help = None self.lang_list = None self.lang_help = None self.default_tags = None self.junit = None self.logging_format = None self.logging_datefmt = None self.name = None self.scope = None self.steps_catalog = None self.userdata = None self.wip = None defaults = self.defaults.copy() for name, value in six.iteritems(kwargs): defaults[name] = value self.defaults = defaults self.formatters = [] self.reporters = [] self.name_re = None self.outputs = [] self.include_re = None self.exclude_re = None self.scenario_outline_annotation_schema = None # pylint: disable=invalid-name self.steps_dir = "steps" self.environment_file = "environment.py" self.userdata_defines = None self.more_formatters = None if load_config: load_configuration(self.defaults, verbose=verbose) parser = setup_parser() parser.set_defaults(**self.defaults) args = parser.parse_args(command_args) for key, value in six.iteritems(args.__dict__): if key.startswith("_") and key not in self.cmdline_only_options: continue setattr(self, key, value) self.paths = [os.path.normpath(path) for path in self.paths] self.setup_outputs(args.outfiles) if self.steps_catalog: # -- SHOW STEP-CATALOG: As step summary. self.default_format = "steps.catalog" self.format = ["steps.catalog"] self.dry_run = True self.summary = False self.show_skipped = False self.quiet = True if self.wip: # Only run scenarios tagged with "wip". # Additionally: # * use the "plain" formatter (per default) # * do not capture stdout or logging output and # * stop at the first failure. self.default_format = "plain" self.tags = ["wip"] + self.default_tags.split() self.color = False self.stop = True self.log_capture = False self.stdout_capture = False self.tags = TagExpression(self.tags or self.default_tags.split()) if self.quiet: self.show_source = False self.show_snippets = False if self.exclude_re: self.exclude_re = re.compile(self.exclude_re) if self.include_re: self.include_re = re.compile(self.include_re) if self.name: # -- SELECT: Scenario-by-name, build regular expression. self.name_re = self.build_name_re(self.name) if self.stage is None: # pylint: disable=access-member-before-definition # -- USE ENVIRONMENT-VARIABLE, if stage is undefined. self.stage = os.environ.get("BEHAVE_STAGE", None) self.setup_stage(self.stage) self.setup_model() self.setup_userdata() # -- FINALLY: Setup Reporters and Formatters # NOTE: Reporters and Formatters can now use userdata information. if self.junit: # Buffer the output (it will be put into Junit report) self.stdout_capture = True self.stderr_capture = True self.log_capture = True self.reporters.append(JUnitReporter(self)) if self.summary: self.reporters.append(SummaryReporter(self)) self.setup_formats() unknown_formats = self.collect_unknown_formats() if unknown_formats: parser.error("format=%s is unknown" % ", ".join(unknown_formats)) def setup_outputs(self, args_outfiles=None): if self.outputs: assert not args_outfiles, "ONLY-ONCE" return # -- NORMAL CASE: Setup only initially (once). if not args_outfiles: self.outputs.append(StreamOpener(stream=sys.stdout)) else: for outfile in args_outfiles: if outfile and outfile != "-": self.outputs.append(StreamOpener(outfile)) else: self.outputs.append(StreamOpener(stream=sys.stdout)) def setup_formats(self): """Register more, user-defined formatters by name.""" if self.more_formatters: for name, scoped_class_name in self.more_formatters.items(): _format_registry.register_as(name, scoped_class_name) def collect_unknown_formats(self): unknown_formats = [] if self.format: for format_name in self.format: if (format_name == "help" or _format_registry.is_formatter_valid(format_name)): continue unknown_formats.append(format_name) return unknown_formats @staticmethod def build_name_re(names): """ Build regular expression for scenario selection by name by using a list of name parts or name regular expressions. :param names: List of name parts or regular expressions (as text). :return: Compiled regular expression to use. """ # -- NOTE: re.LOCALE is removed in Python 3.6 (deprecated in Python 3.5) # flags = (re.UNICODE | re.LOCALE) # -- ENSURE: Names are all unicode/text values (for issue #606). names = to_texts(names) pattern = u"|".join(names) return re.compile(pattern, flags=re.UNICODE) def exclude(self, filename): if isinstance(filename, FileLocation): filename = six.text_type(filename) if self.include_re and self.include_re.search(filename) is None: return True if self.exclude_re and self.exclude_re.search(filename) is not None: return True return False def setup_logging(self, level=None, configfile=None, **kwargs): """ Support simple setup of logging subsystem. Ensures that the logging level is set. But note that the logging setup can only occur once. SETUP MODES: * :func:`logging.config.fileConfig()`, if ``configfile`` is provided. * :func:`logging.basicConfig()`, otherwise. .. code-block: python # -- FILE: features/environment.py def before_all(context): context.config.setup_logging() :param level: Logging level of root logger. If None, use :attr:`logging_level` value. :param configfile: Configuration filename for fileConfig() setup. :param kwargs: Passed to :func:`logging.basicConfig()` """ if level is None: level = self.logging_level # pylint: disable=no-member if configfile: from logging.config import fileConfig fileConfig(configfile) else: # pylint: disable=no-member format_ = kwargs.pop("format", self.logging_format) datefmt = kwargs.pop("datefmt", self.logging_datefmt) logging.basicConfig(format=format_, datefmt=datefmt, **kwargs) # -- ENSURE: Default log level is set # (even if logging subsystem is already configured). logging.getLogger().setLevel(level) def setup_model(self): if self.scenario_outline_annotation_schema: name_schema = six.text_type( self.scenario_outline_annotation_schema) ScenarioOutline.annotation_schema = name_schema.strip() def setup_stage(self, stage=None): """Setup the test stage that selects a different set of steps and environment implementations. :param stage: Name of current test stage (as string or None). EXAMPLE:: # -- SETUP DEFAULT TEST STAGE (unnamed): config = Configuration() config.setup_stage() assert config.steps_dir == "steps" assert config.environment_file == "environment.py" # -- SETUP PRODUCT TEST STAGE: config.setup_stage("product") assert config.steps_dir == "product_steps" assert config.environment_file == "product_environment.py" """ steps_dir = "steps" environment_file = "environment.py" if stage: # -- USE A TEST STAGE: Select different set of implementations. prefix = stage + "_" steps_dir = prefix + steps_dir environment_file = prefix + environment_file self.steps_dir = steps_dir self.environment_file = environment_file def setup_userdata(self): if not isinstance(self.userdata, UserData): self.userdata = UserData(self.userdata) if self.userdata_defines: # -- ENSURE: Cmd-line overrides configuration file parameters. self.userdata.update(self.userdata_defines) def update_userdata(self, data): """Update userdata with data and reapply userdata defines (cmdline). :param data: Provides (partial) userdata (as dict) """ self.userdata.update(data) if self.userdata_defines: # -- REAPPLY: Cmd-line defines (override configuration file data). self.userdata.update(self.userdata_defines)