Angelos Evripiotis pushed to branch aevri/prompt-config at BuildStream / buildstream
Commits:
- 
67d855d1
by Angelos Evripiotis at 2018-11-15T10:31:00Z
- 
31fd24b6
by Angelos Evripiotis at 2018-11-15T10:54:41Z
- 
c4e88413
by Angelos Evripiotis at 2018-11-16T11:07:33Z
- 
b8a77e47
by Angelos Evripiotis at 2018-11-16T11:08:11Z
- 
02d4223a
by Angelos Evripiotis at 2018-11-16T11:08:11Z
6 changed files:
- NEWS
- buildstream/_context.py
- buildstream/_frontend/app.py
- buildstream/_frontend/cli.py
- buildstream/_yaml.py
- buildstream/data/userconfig.yaml
Changes:
| ... | ... | @@ -45,6 +45,12 @@ buildstream 1.3.1 | 
| 45 | 45 |      instead of just a specially-formatted build-root with a `root` and `scratch`
 | 
| 46 | 46 |      subdirectory.
 | 
| 47 | 47 |  | 
| 48 | +  o The buildstream.conf file learned new 'prompt.auto-init',
 | |
| 49 | +    'prompt.really-workspace-close-remove-dir', and
 | |
| 50 | +    'prompt.really-workspace-reset-hard' options. These allow users to suppress
 | |
| 51 | +    certain confirmation prompts, e.g.  double-checking that the user meant to
 | |
| 52 | +    run the command as typed.
 | |
| 53 | + | |
| 48 | 54 |  | 
| 49 | 55 |  =================
 | 
| 50 | 56 |  buildstream 1.1.5
 | 
| ... | ... | @@ -104,6 +104,18 @@ class Context(): | 
| 104 | 104 |          # What to do when a build fails in non interactive mode
 | 
| 105 | 105 |          self.sched_error_action = 'continue'
 | 
| 106 | 106 |  | 
| 107 | +        # Boolean, whether to offer to create a project for the user, if we are
 | |
| 108 | +        # invoked outside of a directory where we can resolve the project.
 | |
| 109 | +        self.prompt_auto_init = None
 | |
| 110 | + | |
| 111 | +        # Boolean, whether we double-check with the user that they meant to
 | |
| 112 | +        # remove a workspace directory.
 | |
| 113 | +        self.prompt_workspace_close_remove_dir = None
 | |
| 114 | + | |
| 115 | +        # Boolean, whether we double-check with the user that they meant to do
 | |
| 116 | +        # a hard reset of a workspace, potentially losing changes.
 | |
| 117 | +        self.prompt_workspace_reset_hard = None
 | |
| 118 | + | |
| 107 | 119 |          # Whether elements must be rebuilt when their dependencies have changed
 | 
| 108 | 120 |          self._strict_build_plan = None
 | 
| 109 | 121 |  | 
| ... | ... | @@ -160,7 +172,7 @@ class Context(): | 
| 160 | 172 |          _yaml.node_validate(defaults, [
 | 
| 161 | 173 |              'sourcedir', 'builddir', 'artifactdir', 'logdir',
 | 
| 162 | 174 |              'scheduler', 'artifacts', 'logging', 'projects',
 | 
| 163 | -            'cache'
 | |
| 175 | +            'cache', 'prompt'
 | |
| 164 | 176 |          ])
 | 
| 165 | 177 |  | 
| 166 | 178 |          for directory in ['sourcedir', 'builddir', 'artifactdir', 'logdir']:
 | 
| ... | ... | @@ -206,12 +218,34 @@ class Context(): | 
| 206 | 218 |              'on-error', 'fetchers', 'builders',
 | 
| 207 | 219 |              'pushers', 'network-retries'
 | 
| 208 | 220 |          ])
 | 
| 209 | -        self.sched_error_action = _yaml.node_get(scheduler, str, 'on-error')
 | |
| 221 | +        self.sched_error_action = _node_get_option_str(
 | |
| 222 | +            scheduler, 'on-error', ['continue', 'quit', 'terminate'])
 | |
| 210 | 223 |          self.sched_fetchers = _yaml.node_get(scheduler, int, 'fetchers')
 | 
| 211 | 224 |          self.sched_builders = _yaml.node_get(scheduler, int, 'builders')
 | 
| 212 | 225 |          self.sched_pushers = _yaml.node_get(scheduler, int, 'pushers')
 | 
| 213 | 226 |          self.sched_network_retries = _yaml.node_get(scheduler, int, 'network-retries')
 | 
| 214 | 227 |  | 
| 228 | +        # Load prompt preferences
 | |
| 229 | +        #
 | |
| 230 | +        # We convert string options to booleans here, so we can be both user
 | |
| 231 | +        # and coder-friendly. The string options are worded to match the
 | |
| 232 | +        # responses the user would give at the cli, for least surprise. The
 | |
| 233 | +        # booleans are converted here because it's easiest to eyeball that the
 | |
| 234 | +        # strings are right.
 | |
| 235 | +        #
 | |
| 236 | +        prompt = _yaml.node_get(
 | |
| 237 | +            defaults, Mapping, 'prompt')
 | |
| 238 | +        _yaml.node_validate(prompt, [
 | |
| 239 | +            'auto-init', 'really-workspace-close-remove-dir',
 | |
| 240 | +            'really-workspace-reset-hard',
 | |
| 241 | +        ])
 | |
| 242 | +        self.prompt_auto_init = _node_get_option_str(
 | |
| 243 | +            prompt, 'auto-init', ['ask', 'no']) == 'ask'
 | |
| 244 | +        self.prompt_workspace_close_remove_dir = _node_get_option_str(
 | |
| 245 | +            prompt, 'really-workspace-close-remove-dir', ['ask', 'yes']) == 'ask'
 | |
| 246 | +        self.prompt_workspace_reset_hard = _node_get_option_str(
 | |
| 247 | +            prompt, 'really-workspace-reset-hard', ['ask', 'yes']) == 'ask'
 | |
| 248 | + | |
| 215 | 249 |          # Load per-projects overrides
 | 
| 216 | 250 |          self._project_overrides = _yaml.node_get(defaults, Mapping, 'projects', default_value={})
 | 
| 217 | 251 |  | 
| ... | ... | @@ -222,13 +256,6 @@ class Context(): | 
| 222 | 256 |  | 
| 223 | 257 |          profile_end(Topics.LOAD_CONTEXT, 'load')
 | 
| 224 | 258 |  | 
| 225 | -        valid_actions = ['continue', 'quit']
 | |
| 226 | -        if self.sched_error_action not in valid_actions:
 | |
| 227 | -            provenance = _yaml.node_get_provenance(scheduler, 'on-error')
 | |
| 228 | -            raise LoadError(LoadErrorReason.INVALID_DATA,
 | |
| 229 | -                            "{}: on-error should be one of: {}".format(
 | |
| 230 | -                                provenance, ", ".join(valid_actions)))
 | |
| 231 | - | |
| 232 | 259 |      @property
 | 
| 233 | 260 |      def artifactcache(self):
 | 
| 234 | 261 |          if not self._artifactcache:
 | 
| ... | ... | @@ -581,3 +608,30 @@ class Context(): | 
| 581 | 608 |              os.environ['XDG_CONFIG_HOME'] = os.path.expanduser('~/.config')
 | 
| 582 | 609 |          if not os.environ.get('XDG_DATA_HOME'):
 | 
| 583 | 610 |              os.environ['XDG_DATA_HOME'] = os.path.expanduser('~/.local/share')
 | 
| 611 | + | |
| 612 | + | |
| 613 | +# _node_get_option_str()
 | |
| 614 | +#
 | |
| 615 | +# Fetches a value from a dictionary node, and makes sure it's one of the
 | |
| 616 | +# pre-defined options.
 | |
| 617 | +#
 | |
| 618 | +# Args:
 | |
| 619 | +#    node (dict): The dictionary node
 | |
| 620 | +#    key (str): The key to get a value for in node
 | |
| 621 | +#    allowed_options (iterable): Only accept these values
 | |
| 622 | +#
 | |
| 623 | +# Returns:
 | |
| 624 | +#    The value if found in node
 | |
| 625 | +#
 | |
| 626 | +# Raises:
 | |
| 627 | +#    LoadError, when the value found is not of the expected type, or is not
 | |
| 628 | +#    found.
 | |
| 629 | +#
 | |
| 630 | +def _node_get_option_str(node, key, allowed_options):
 | |
| 631 | +        result = _yaml.node_get(node, str, key)
 | |
| 632 | +        if result not in allowed_options:
 | |
| 633 | +            provenance = _yaml.node_get_provenance(node, key)
 | |
| 634 | +            raise LoadError(LoadErrorReason.INVALID_DATA,
 | |
| 635 | +                            "{}: {} should be one of: {}".format(
 | |
| 636 | +                                provenance, key, ", ".join(allowed_options)))
 | |
| 637 | +        return result | 
| ... | ... | @@ -221,9 +221,10 @@ class App(): | 
| 221 | 221 |              # Let's automatically start a `bst init` session in this case
 | 
| 222 | 222 |              if e.reason == LoadErrorReason.MISSING_PROJECT_CONF and self.interactive:
 | 
| 223 | 223 |                  click.echo("A project was not detected in the directory: {}".format(directory), err=True)
 | 
| 224 | -                click.echo("", err=True)
 | |
| 225 | -                if click.confirm("Would you like to create a new project here ?"):
 | |
| 226 | -                    self.init_project(None)
 | |
| 224 | +                if self.context.prompt_auto_init:
 | |
| 225 | +                    click.echo("", err=True)
 | |
| 226 | +                    if click.confirm("Would you like to create a new project here?"):
 | |
| 227 | +                        self.init_project(None)
 | |
| 227 | 228 |  | 
| 228 | 229 |              self._error_exit(e, "Error loading project")
 | 
| 229 | 230 |  | 
| ... | ... | @@ -743,7 +743,7 @@ def workspace_close(app, remove_dir, all_, elements): | 
| 743 | 743 |          if nonexisting:
 | 
| 744 | 744 |              raise AppError("Workspace does not exist", detail="\n".join(nonexisting))
 | 
| 745 | 745 |  | 
| 746 | -        if app.interactive and remove_dir:
 | |
| 746 | +        if app.interactive and remove_dir and app.context.prompt_workspace_close_remove_dir:
 | |
| 747 | 747 |              if not click.confirm('This will remove all your changes, are you sure?'):
 | 
| 748 | 748 |                  click.echo('Aborting', err=True)
 | 
| 749 | 749 |                  sys.exit(-1)
 | 
| ... | ... | @@ -777,7 +777,7 @@ def workspace_reset(app, soft, track_, all_, elements): | 
| 777 | 777 |          if all_ and not app.stream.workspace_exists():
 | 
| 778 | 778 |              raise AppError("No open workspaces to reset")
 | 
| 779 | 779 |  | 
| 780 | -        if app.interactive and not soft:
 | |
| 780 | +        if app.interactive and not soft and app.context.prompt_workspace_reset_hard:
 | |
| 781 | 781 |              if not click.confirm('This will remove all your changes, are you sure?'):
 | 
| 782 | 782 |                  click.echo('Aborting', err=True)
 | 
| 783 | 783 |                  sys.exit(-1)
 | 
| ... | ... | @@ -351,6 +351,7 @@ _sentinel = object() | 
| 351 | 351 |  #    expected_type (type): The expected type for the value being searched
 | 
| 352 | 352 |  #    key (str): The key to get a value for in node
 | 
| 353 | 353 |  #    indices (list of ints): Optionally decend into lists of lists
 | 
| 354 | +#    default_value: Optionally return this value if the key is not found
 | |
| 354 | 355 |  #
 | 
| 355 | 356 |  # Returns:
 | 
| 356 | 357 |  #    The value if found in node, otherwise default_value is returned
 | 
| ... | ... | @@ -97,3 +97,35 @@ logging: | 
| 97 | 97 |  | 
| 98 | 98 |      [%{elapsed}][%{key}][%{element}] %{action} %{message}
 | 
| 99 | 99 |  | 
| 100 | +#
 | |
| 101 | +#    Prompt overrides
 | |
| 102 | +#
 | |
| 103 | +# Here you can suppress 'are you sure?' and other kinds of prompts by supplying
 | |
| 104 | +# override values. Note that e.g. 'yes' and 'no' have the same meaning here as
 | |
| 105 | +# they do in the actual prompt.
 | |
| 106 | +#
 | |
| 107 | +prompt:
 | |
| 108 | + | |
| 109 | +  # Whether to create a project with 'bst init' if we are invoked outside of a
 | |
| 110 | +  # directory where we can resolve the project.
 | |
| 111 | +  #
 | |
| 112 | +  #  ask - Prompt the user to choose.
 | |
| 113 | +  #  no  - Never create the project.
 | |
| 114 | +  #
 | |
| 115 | +  auto-init: ask
 | |
| 116 | + | |
| 117 | +  # Whether to really proceed with 'bst workspace close --remove-dir' removing
 | |
| 118 | +  # a workspace directory, potentially losing changes.
 | |
| 119 | +  #
 | |
| 120 | +  #  ask - Ask the user if they are sure.
 | |
| 121 | +  #  yes - Always remove, without asking.
 | |
| 122 | +  #
 | |
| 123 | +  really-workspace-close-remove-dir: ask
 | |
| 124 | + | |
| 125 | +  # Whether to really proceed with 'bst workspace reset' doing a hard reset of
 | |
| 126 | +  # a workspace, potentially losing changes.
 | |
| 127 | +  #
 | |
| 128 | +  #  ask - Ask the user if they are sure.
 | |
| 129 | +  #  yes - Always hard reset, without asking.
 | |
| 130 | +  #
 | |
| 131 | +  really-workspace-reset-hard: ask | 
