From 705b27f3b715c75fac2242c5ed0e66b03feeea1f Mon Sep 17 00:00:00 2001 From: Havoc Pennington Date: Wed, 17 May 2017 15:49:30 -0400 Subject: [PATCH] Remove browser UI mode. In this PR, keeping around read_config/set_config_values_as_strings even though only the HTML UI really used those, because in theory we might want to use them to make a similar textual UI (or an HTML UI that isn't included in the anaconda-project codebase). This commit can be referenced as a guide to building a textual UI in the future. --- anaconda_project/api.py | 40 -- .../internal/cli/prepare_with_mode.py | 100 ++- .../internal/cli/test/test_prepare.py | 139 ---- anaconda_project/internal/plugin_html.py | 97 --- anaconda_project/internal/prepare_ui.py | 55 -- .../test/test_default_conda_manager.py | 8 + .../internal/test/test_plugin_html.py | 268 -------- .../internal/test/test_ui_server.py | 171 ----- anaconda_project/internal/ui_server.py | 254 ------- anaconda_project/plugins/provider.py | 106 +-- .../plugins/providers/conda_env.py | 33 - .../plugins/providers/download.py | 22 - anaconda_project/plugins/providers/redis.py | 32 - .../plugins/providers/test/test_conda_env.py | 625 +++--------------- .../providers/test/test_download_provider.py | 405 ++---------- .../providers/test/test_redis_provider.py | 2 - .../plugins/test/test_provider.py | 47 -- anaconda_project/prepare.py | 91 --- anaconda_project/test/test_api.py | 28 - anaconda_project/test/test_prepare.py | 242 +------ conda.recipe/meta.yaml | 2 - setup.py | 2 +- 22 files changed, 251 insertions(+), 2518 deletions(-) delete mode 100644 anaconda_project/internal/plugin_html.py delete mode 100644 anaconda_project/internal/prepare_ui.py delete mode 100644 anaconda_project/internal/test/test_plugin_html.py delete mode 100644 anaconda_project/internal/test/test_ui_server.py delete mode 100644 anaconda_project/internal/ui_server.py diff --git a/anaconda_project/api.py b/anaconda_project/api.py index a743a095..b9d2d979 100644 --- a/anaconda_project/api.py +++ b/anaconda_project/api.py @@ -218,46 +218,6 @@ def prepare_project_check(self, command=command, extra_command_args=extra_command_args) - def prepare_project_browser(self, - project, - environ, - env_spec_name=None, - command_name=None, - command=None, - extra_command_args=None, - io_loop=None, - show_url=None): - """Prepare a project to run one of its commands. - - This version uses a browser-based UI to allow the user to - see and choose how to meet project requirements. - - See ``prepare_project_locally()`` for additional details - that also apply to this method. - - Args: - project (Project): from the ``load_project`` method - environ (dict): os.environ or the previously-prepared environ; not modified in-place - env_spec_name (str): the package set name to require, or None for default - command_name (str): which named command to choose from the project, None for default - command (ProjectCommand): a command object (alternative to command_name) - extra_command_args (list): extra args to include in the returned command argv - io_loop (IOLoop): tornado IOLoop to use, None for default - show_url (function): function that's passed the URL to open it for the user - - Returns: - a ``PrepareResult`` instance, which has a ``failed`` flag - - """ - return prepare.prepare_with_browser_ui(project=project, - environ=environ, - env_spec_name=env_spec_name, - command_name=command_name, - command=command, - extra_command_args=extra_command_args, - io_loop=io_loop, - show_url=show_url) - def unprepare(self, project, prepare_result, whitelist=None): """Attempt to clean up project-scoped resources allocated by prepare(). diff --git a/anaconda_project/internal/cli/prepare_with_mode.py b/anaconda_project/internal/cli/prepare_with_mode.py index 197adbd9..93359cc3 100644 --- a/anaconda_project/internal/cli/prepare_with_mode.py +++ b/anaconda_project/internal/cli/prepare_with_mode.py @@ -18,7 +18,6 @@ # these UI_MODE_ strings are used as values for command line options, so they are user-visible -UI_MODE_BROWSER = "browser" # ASK_QUESTIONS mode is supposed to ask about default actions too, # like whether to start servers. It isn't implemented yet. UI_MODE_TEXT_ASK_QUESTIONS = "ask" @@ -27,7 +26,7 @@ UI_MODE_TEXT_ASSUME_YES_DEVELOPMENT = "development_defaults" UI_MODE_TEXT_ASSUME_NO = "check" -_all_ui_modes = (UI_MODE_BROWSER, UI_MODE_TEXT_ASK_QUESTIONS, UI_MODE_TEXT_DEVELOPMENT_DEFAULTS_OR_ASK, +_all_ui_modes = (UI_MODE_TEXT_ASK_QUESTIONS, UI_MODE_TEXT_DEVELOPMENT_DEFAULTS_OR_ASK, UI_MODE_TEXT_ASSUME_YES_PRODUCTION, UI_MODE_TEXT_ASSUME_YES_DEVELOPMENT, UI_MODE_TEXT_ASSUME_NO) @@ -90,7 +89,7 @@ def prepare_with_ui_mode_printing_errors(project, Args: project (Project): the project environ (dict): the environment to prepare (None to use os.environ) - ui_mode (str): one of ``UI_MODE_BROWSER``, ``UI_MODE_TEXT_ASSUME_YES_DEVELOPMENT``, + ui_mode (str): one of ``UI_MODE_TEXT_ASSUME_YES_DEVELOPMENT``, ``UI_MODE_TEXT_ASSUME_YES_PRODUCTION``, ``UI_MODE_TEXT_ASSUME_NO`` env_spec_name (str): the environment spec name to require, or None for default command_name (str): command name to use or None for default @@ -103,54 +102,51 @@ def prepare_with_ui_mode_printing_errors(project, """ assert ui_mode in _all_ui_modes # the arg parser should have guaranteed this - if ui_mode == UI_MODE_BROWSER: - result = prepare.prepare_with_browser_ui(project, - environ, - env_spec_name=env_spec_name, - command_name=command_name, - command=command, - extra_command_args=extra_command_args, - keep_going_until_success=True) - else: - ask = False - if ui_mode == UI_MODE_TEXT_ASSUME_YES_PRODUCTION: - provide_mode = PROVIDE_MODE_PRODUCTION - elif ui_mode == UI_MODE_TEXT_ASSUME_YES_DEVELOPMENT: - provide_mode = PROVIDE_MODE_DEVELOPMENT - elif ui_mode == UI_MODE_TEXT_ASSUME_NO: - provide_mode = PROVIDE_MODE_CHECK - elif ui_mode == UI_MODE_TEXT_DEVELOPMENT_DEFAULTS_OR_ASK: - provide_mode = PROVIDE_MODE_DEVELOPMENT - ask = True - - assert ui_mode != UI_MODE_TEXT_ASK_QUESTIONS # Not implemented yet - - # TODO: this could let you fix the suggestions if they are fixable. - # (Note that we fix fatal problems in project_load.py, but we only - # display suggestions when we do a manual prepare, run, etc.) - suggestions = project.suggestions - if len(suggestions) > 0: - print("Potential issues with this project:") - for suggestion in project.suggestions: - print(" * " + suggestion) - print("") - - environ = None - while True: - result = prepare.prepare_without_interaction(project, - environ, - mode=provide_mode, - env_spec_name=env_spec_name, - command_name=command_name, - command=command, - extra_command_args=extra_command_args) - - if result.failed: - if ask and _interactively_fix_missing_variables(project, result): - environ = result.environ - continue # re-prepare, building on our previous environ - - # if we didn't continue, quit. - break + ask = False + if ui_mode == UI_MODE_TEXT_ASSUME_YES_PRODUCTION: + provide_mode = PROVIDE_MODE_PRODUCTION + elif ui_mode == UI_MODE_TEXT_ASSUME_YES_DEVELOPMENT: + provide_mode = PROVIDE_MODE_DEVELOPMENT + elif ui_mode == UI_MODE_TEXT_ASSUME_NO: + provide_mode = PROVIDE_MODE_CHECK + elif ui_mode == UI_MODE_TEXT_DEVELOPMENT_DEFAULTS_OR_ASK: + provide_mode = PROVIDE_MODE_DEVELOPMENT + ask = True + + # We might implement this by using + # Provider.read_config/Provider.set_config_values_as_strings + # or some new version of those; dig the old ui_server.py out + # of git history to see how we used those methods to implement + # an interactive HTML UI. read_config/set_config_values still + # exist on Provider in case they are useful to implement this. + assert ui_mode != UI_MODE_TEXT_ASK_QUESTIONS # Not implemented yet + + # TODO: this could let you fix the suggestions if they are fixable. + # (Note that we fix fatal problems in project_load.py, but we only + # display suggestions when we do a manual prepare, run, etc.) + suggestions = project.suggestions + if len(suggestions) > 0: + print("Potential issues with this project:") + for suggestion in project.suggestions: + print(" * " + suggestion) + print("") + + environ = None + while True: + result = prepare.prepare_without_interaction(project, + environ, + mode=provide_mode, + env_spec_name=env_spec_name, + command_name=command_name, + command=command, + extra_command_args=extra_command_args) + + if result.failed: + if ask and _interactively_fix_missing_variables(project, result): + environ = result.environ + continue # re-prepare, building on our previous environ + + # if we didn't continue, quit. + break return result diff --git a/anaconda_project/internal/cli/test/test_prepare.py b/anaconda_project/internal/cli/test/test_prepare.py index f9a65973..a768d293 100644 --- a/anaconda_project/internal/cli/test/test_prepare.py +++ b/anaconda_project/internal/cli/test/test_prepare.py @@ -74,144 +74,6 @@ def test_prepare_command_assume_no(monkeypatch): _test_prepare_command(monkeypatch, UI_MODE_TEXT_ASSUME_NO) -def _form_names(response, provider): - from anaconda_project.internal.plugin_html import _BEAUTIFUL_SOUP_BACKEND - from bs4 import BeautifulSoup - - if response.code != 200: - raise Exception("got a bad http response " + repr(response)) - - soup = BeautifulSoup(response.body, _BEAUTIFUL_SOUP_BACKEND) - named_elements = soup.find_all(attrs={'name': True}) - names = set() - for element in named_elements: - if provider in element['name']: - names.add(element['name']) - return names - - -def _prefix_form(form_names, form): - prefixed = dict() - for (key, value) in form.items(): - found = False - for name in form_names: - if name.endswith("." + key): - prefixed[name] = value - found = True - break - if not found: - raise RuntimeError("Form field %s in %r could not be prefixed from %r" % (key, form, form_names)) - return prefixed - - -def _monkeypatch_open_new_tab(monkeypatch): - from tornado.ioloop import IOLoop - - http_results = {} - - def mock_open_new_tab(url): - from anaconda_project.internal.test.http_utils import http_get_async, http_post_async - from tornado import gen - - @gen.coroutine - def do_http(): - http_results['get'] = yield http_get_async(url) - - # pick our environment (using inherited one) - form_names = _form_names(http_results['get'], provider='CondaEnvProvider') - form = _prefix_form(form_names, {'source': 'inherited'}) - response = yield http_post_async(url, form=form) - assert response.code == 200 - - # now do the next round of stuff - http_results['post'] = yield http_post_async(url, body="") - - IOLoop.current().add_callback(do_http) - - monkeypatch.setattr('webbrowser.open_new_tab', mock_open_new_tab) - - return http_results - - -def test_main(monkeypatch, capsys): - can_connect_args = _monkeypatch_can_connect_to_socket_to_succeed(monkeypatch) - _monkeypatch_open_new_tab(monkeypatch) - - def mock_conda_create(prefix, pkgs, channels, stdout_callback, stderr_callback): - raise RuntimeError("this test should not create an environment in %s with pkgs %r" % (prefix, pkgs)) - - monkeypatch.setattr('anaconda_project.internal.conda_api.create', mock_conda_create) - - def main_redis_url(dirname): - project_dir_disable_dedicated_env(dirname) - main(Args(directory=dirname, mode='browser')) - - with_directory_contents_completing_project_file( - {DEFAULT_PROJECT_FILENAME: """ -services: - REDIS_URL: redis -"""}, main_redis_url) - - assert can_connect_args['port'] == 6379 - - out, err = capsys.readouterr() - assert "# Configure the project at " in out - assert "" == err - - -def test_main_dirname_not_provided_use_pwd(monkeypatch, capsys): - can_connect_args = _monkeypatch_can_connect_to_socket_to_succeed(monkeypatch) - _monkeypatch_open_new_tab(monkeypatch) - - def main_redis_url(dirname): - from os.path import abspath as real_abspath - - def mock_abspath(path): - if path == ".": - return dirname - else: - return real_abspath(path) - - monkeypatch.setattr('os.path.abspath', mock_abspath) - project_dir_disable_dedicated_env(dirname) - code = _parse_args_and_run_subcommand(['anaconda-project', 'prepare', '--mode=browser']) - assert code == 0 - - with_directory_contents_completing_project_file( - {DEFAULT_PROJECT_FILENAME: """ -services: - REDIS_URL: redis -"""}, main_redis_url) - - assert can_connect_args['port'] == 6379 - - out, err = capsys.readouterr() - assert "# Configure the project at " in out - assert "" == err - - -def test_main_dirname_provided_use_it(monkeypatch, capsys): - can_connect_args = _monkeypatch_can_connect_to_socket_to_succeed(monkeypatch) - _monkeypatch_open_new_tab(monkeypatch) - - def main_redis_url(dirname): - project_dir_disable_dedicated_env(dirname) - code = _parse_args_and_run_subcommand(['anaconda-project', 'prepare', '--directory', dirname, '--mode=browser']) - assert code == 0 - - with_directory_contents_completing_project_file( - {DEFAULT_PROJECT_FILENAME: """ -services: - REDIS_URL: redis -"""}, main_redis_url) - - assert can_connect_args['port'] == 6379 - - out, err = capsys.readouterr() - assert "# Configure the project at " in out - assert "" == err - - def _monkeypatch_can_connect_to_socket_to_fail_to_find_redis(monkeypatch): def mock_can_connect_to_socket(host, port, timeout_seconds=0.5): if port == 6379: @@ -224,7 +86,6 @@ def mock_can_connect_to_socket(host, port, timeout_seconds=0.5): def test_main_fails_to_redis(monkeypatch, capsys): _monkeypatch_can_connect_to_socket_to_fail_to_find_redis(monkeypatch) - _monkeypatch_open_new_tab(monkeypatch) from anaconda_project.internal.cli.prepare_with_mode import prepare_with_ui_mode_printing_errors as real_prepare diff --git a/anaconda_project/internal/plugin_html.py b/anaconda_project/internal/plugin_html.py deleted file mode 100644 index f23b8e90..00000000 --- a/anaconda_project/internal/plugin_html.py +++ /dev/null @@ -1,97 +0,0 @@ -# -*- coding: utf-8 -*- -# ---------------------------------------------------------------------------- -# Copyright © 2016, Continuum Analytics, Inc. All rights reserved. -# -# The full license is in the file LICENSE.txt, distributed with this software. -# ---------------------------------------------------------------------------- -from __future__ import absolute_import, print_function - -from bs4 import BeautifulSoup - -_FORM_FIELD_ELEMENTS = ['input', 'textarea', 'select'] - -_BEAUTIFUL_SOUP_BACKEND = "html.parser" - - -def _set_element_value(element, value): - assert value is not None - value_string = str(value) - if isinstance(value, bool): - value_bool = value - elif 'value' in element.attrs and element['value'] == value_string: - value_bool = (value_string == element['value']) - else: - value_bool = False - if element.name == 'input': - if element['type'] == 'checkbox': - if value_bool: - element['checked'] = '' - else: - del element['checked'] - elif element['type'] == 'radio': - if value_bool: - element['checked'] = '' - else: - del element['checked'] - elif element['type'] == 'hidden': - # we don't know what to do with these; right now - # we use them as a hack to go next to checkboxes - # and be sure we always send a value for checkbox - # query params - pass - else: - element['value'] = value_string - elif element.name == 'textarea': - element.string = value_string - elif element.name == 'select': - options = element.find_all('option') - for option in options: - if 'value' in option.attrs: - option_string = option['value'] - else: - option_string = option.string - if option_string == value_string: - option['selected'] = '' - else: - del option['selected'] - - -def cleanup_and_scope_form(html, prefix, values): - # - parse the html - # - be sure it's a
tag and dump anything that isn't - # - change form input names to have the prefix - # - set form input current values to the provided ones - # - remove the surrounding and replace with a
- # so we can put it in one big form - soup = BeautifulSoup(html, _BEAUTIFUL_SOUP_BACKEND) - if soup.form is None: - raise ValueError("HTML does not have a root element") - named = [] - for element_name in _FORM_FIELD_ELEMENTS: - named = named + soup.form.find_all(element_name) - for element in named: - if 'name' in element.attrs: - name = element['name'] - element['name'] = prefix + name - value = values.get(name, None) - if value is not None: - _set_element_value(element, value) - else: - import sys - print("No 'name' attribute set on %r" % (element), file=sys.stderr) - - # note that this will dump anything that was in the input other than the . - # don't use prettify() it indents -
-""".strip() - - assert expected == cleaned - - -def test_cleanup_and_scope_form_checkbox_not_checked(): - original = """ - - -
-""" - - cleaned = cleanup_and_scope_form(original, "prefix.", dict(foo="bar")) - - expected = """ -
- -
-""".strip() - - assert expected == cleaned - - -def test_cleanup_and_scope_form_checkbox_checked(): - original = """ -
- -
-""" - - cleaned = cleanup_and_scope_form(original, "prefix.", dict(foo="bar")) - - expected = """ -
- -
-""".strip() - - assert expected == cleaned - - -def test_cleanup_and_scope_form_checkbox_checked_bool_value(): - original = """ -
- -
-""" - - cleaned = cleanup_and_scope_form(original, "prefix.", dict(foo=True)) - - expected = """ -
- -
-""".strip() - - assert expected == cleaned - - -def test_cleanup_and_scope_form_radio(): - original = """ -
- - - -
-""" - - cleaned = cleanup_and_scope_form(original, "prefix.", dict(foo="1")) - - expected = """ -
- - - -
-""".strip() - - assert expected == cleaned - - -def test_cleanup_and_scope_form_select_using_value_attribute(): - original = """ -
- -
-""" - - cleaned = cleanup_and_scope_form(original, "prefix.", dict(foo="1")) - - expected = """ -
- -
-""".strip() - - assert expected == cleaned - - -def test_cleanup_and_scope_form_select_using_element_text(): - original = """ -
- -
-""" - - cleaned = cleanup_and_scope_form(original, "prefix.", dict(foo="1")) - - expected = """ -
- -
-""".strip() - - assert expected == cleaned - - -def test_cleanup_and_scope_form_leave_hidden_alone(): - original = """ -
- -
-""" - - cleaned = cleanup_and_scope_form(original, "prefix.", dict(foo="blah")) - - # we should NOT set the value on a hidden - expected = """ -
- -
-""".strip() - - assert expected == cleaned diff --git a/anaconda_project/internal/test/test_ui_server.py b/anaconda_project/internal/test/test_ui_server.py deleted file mode 100644 index b51b8af9..00000000 --- a/anaconda_project/internal/test/test_ui_server.py +++ /dev/null @@ -1,171 +0,0 @@ -# -*- coding: utf-8 -*- -# ---------------------------------------------------------------------------- -# Copyright © 2016, Continuum Analytics, Inc. All rights reserved. -# -# The full license is in the file LICENSE.txt, distributed with this software. -# ---------------------------------------------------------------------------- -from __future__ import absolute_import, print_function - -from bs4 import BeautifulSoup -from tornado.ioloop import IOLoop - -from anaconda_project.internal.plugin_html import _BEAUTIFUL_SOUP_BACKEND -from anaconda_project.project import Project -from anaconda_project.prepare import ConfigurePrepareContext, _FunctionPrepareStage, PrepareSuccess -from anaconda_project.internal.test.http_utils import http_get, http_post -from anaconda_project.internal.test.multipart import MultipartEncoder -from anaconda_project.internal.test.tmpfile_utils import with_directory_contents -from anaconda_project.internal.ui_server import UIServer, UIServerDoneEvent -from anaconda_project.local_state_file import LocalStateFile -from anaconda_project.plugins.requirement import EnvVarRequirement, UserConfigOverrides - - -def _no_op_prepare(config_context): - def _do_nothing(stage): - stage.set_result( - PrepareSuccess(statuses=(), - command_exec_info=None, - environ=dict(), - overrides=UserConfigOverrides(), - env_spec_name='something'), - []) - return None - - return _FunctionPrepareStage(dict(), UserConfigOverrides(), "Do Nothing", [], _do_nothing, config_context) - - -def test_ui_server_empty(): - def do_test(dirname): - io_loop = IOLoop() - io_loop.make_current() - - events = [] - - def event_handler(event): - events.append(event) - - project = Project(dirname) - local_state_file = LocalStateFile.load_for_directory(dirname) - context = ConfigurePrepareContext(dict(), local_state_file, 'default', UserConfigOverrides(), []) - server = UIServer(project, _no_op_prepare(context), event_handler, io_loop) - - get_response = http_get(io_loop, server.url) - print(repr(get_response)) - post_response = http_post(io_loop, server.url, body="") - print(repr(post_response)) - - server.unlisten() - - assert len(events) == 1 - assert isinstance(events[0], UIServerDoneEvent) - - with_directory_contents(dict(), do_test) - - -def test_ui_server_with_form(): - def do_test(dirname): - io_loop = IOLoop() - io_loop.make_current() - - events = [] - - def event_handler(event): - events.append(event) - - local_state_file = LocalStateFile.load_for_directory(dirname) - - value = local_state_file.get_value(['variables', 'FOO']) - assert value is None - - project = Project(dirname) - requirement = EnvVarRequirement(registry=project.plugin_registry, env_var="FOO") - status = requirement.check_status(dict(), local_state_file, 'default', UserConfigOverrides()) - context = ConfigurePrepareContext(dict(), local_state_file, 'default', UserConfigOverrides(), [status]) - server = UIServer(project, _no_op_prepare(context), event_handler, io_loop) - - get_response = http_get(io_loop, server.url) - print(repr(get_response)) - - soup = BeautifulSoup(get_response.body, _BEAUTIFUL_SOUP_BACKEND) - field = soup.find_all("input", attrs={'type': 'text'})[0] - - assert 'name' in field.attrs - - encoder = MultipartEncoder({field['name']: 'bloop'}) - body = encoder.to_string() - headers = {'Content-Type': encoder.content_type} - - post_response = http_post(io_loop, server.url, body=body, headers=headers) - print(repr(post_response)) - - server.unlisten() - - assert len(events) == 1 - assert isinstance(events[0], UIServerDoneEvent) - - value = local_state_file.get_value(['variables', 'FOO']) - assert 'bloop' == value - - with_directory_contents(dict(), do_test) - - -def _ui_server_bad_form_name_test(capsys, name_template, expected_err): - def do_test(dirname): - io_loop = IOLoop() - io_loop.make_current() - - events = [] - - def event_handler(event): - events.append(event) - - project = Project(dirname) - local_state_file = LocalStateFile.load_for_directory(dirname) - - requirement = EnvVarRequirement(registry=project.plugin_registry, env_var="FOO") - status = requirement.check_status(dict(), local_state_file, 'default', UserConfigOverrides()) - context = ConfigurePrepareContext(dict(), local_state_file, 'default', UserConfigOverrides(), [status]) - server = UIServer(project, _no_op_prepare(context), event_handler, io_loop) - - # do a get so that _requirements_by_id below exists - get_response = http_get(io_loop, server.url) - assert 200 == get_response.code - - req_id = list(server._application._requirements_by_id.keys())[0] - if '%s' in name_template: - name = name_template % req_id - else: - name = name_template - - encoder = MultipartEncoder({name: 'bloop'}) - body = encoder.to_string() - headers = {'Content-Type': encoder.content_type} - - post_response = http_post(io_loop, server.url, body=body, headers=headers) - # we just ignore bad form names, because they are assumed - # to be some sort of hostile thing. we shouldn't ever - # generate them on purpose. - assert 200 == post_response.code - - server.unlisten() - - assert len(events) == 1 - assert isinstance(events[0], UIServerDoneEvent) - - out, err = capsys.readouterr() - assert out == "" - assert err == expected_err - - with_directory_contents(dict(), do_test) - - -def test_ui_server_not_enough_pieces_in_posted_name(capsys): - _ui_server_bad_form_name_test(capsys, "nopieces", "not enough pieces in ['nopieces']\n") - - -def test_ui_server_invalid_req_id_in_posted_name(capsys): - _ui_server_bad_form_name_test(capsys, "badid.EnvVarProvider.value", "badid not a known requirement id\n") - - -def test_ui_server_invalid_provider_key_in_posted_name(capsys): - _ui_server_bad_form_name_test(capsys, "%s.BadProvider.value", "did not find provider BadProvider\n") diff --git a/anaconda_project/internal/ui_server.py b/anaconda_project/internal/ui_server.py deleted file mode 100644 index bdb67c89..00000000 --- a/anaconda_project/internal/ui_server.py +++ /dev/null @@ -1,254 +0,0 @@ -# -*- coding: utf-8 -*- -# ---------------------------------------------------------------------------- -# Copyright © 2016, Continuum Analytics, Inc. All rights reserved. -# -# The full license is in the file LICENSE.txt, distributed with this software. -# ---------------------------------------------------------------------------- -from __future__ import absolute_import, print_function - -import collections -import socket -import sys -import uuid - -from tornado.httpserver import HTTPServer -from tornado.netutil import bind_sockets -from tornado.web import Application, RequestHandler - -from anaconda_project.internal.plugin_html import cleanup_and_scope_form, html_tag - - -class UIServerEvent(object): - pass - - -class UIServerDoneEvent(UIServerEvent): - def __init__(self, result): - super(UIServerDoneEvent, self).__init__() - self.result = result - -# future: use actual template system -# it's important to replace & before the later ones -_entity_table = [("&", "&"), ("<", "<"), (">", ">"), ("'", "'"), ('"', """)] - - -def _html_escape(text): - for (key, value) in _entity_table: - text = text.replace(key, value) - return text - - -class PrepareViewHandler(RequestHandler): - def __init__(self, application, *args, **kwargs): - # Note: application is stored as self.application - super(PrepareViewHandler, self).__init__(application, *args, **kwargs) - - def _outer_page(self, content): - return """ - - - - - Project setup for %s - - - %s - - -""" % (self.application.project.name, content) - - def _html_for_status_list(self, statuses, with_config, prepare_context=None): - html = "" - - return html - - def _result_page(self, result, latest_statuses): - # TODO: clean this up, we should show the usual status - # list with errors embedded and possibly config html to - # fix them, rather than showing just textual errors free - # of context. - if result.failed: - error_html = """ -

Something didn't work...

-\n" - - return self._outer_page(error_html) - else: - status_list_html = self._html_for_status_list(latest_statuses, with_config=False) - return self._outer_page(""" -
Done! Close this window now if you like.
-""" + status_list_html) - - def get(self, *args, **kwargs): - if self.application.prepare_stage is None: - self.application.emit_event(UIServerDoneEvent(result=self.application.last_stage_result)) - page = self._result_page(self.application.last_stage_result, self.application.latest_statuses) - else: - prepare_context = self.application.prepare_stage.configure() - - config_html = "" - - if prepare_context is not None: - - self.application.refresh_form_ids(prepare_context) - - status_list_html = self._html_for_status_list(prepare_context.statuses, - with_config=True, - prepare_context=prepare_context) - - config_html = config_html + status_list_html - - page = self._outer_page(""" -
-
-

Project "%s" has these requirements that may need setup:

- %s - -
-
-""" % (self.application.project.name, config_html, self.application.prepare_stage.description_of_action)) - - self.set_header("Content-Type", 'text/html') - self.write(page) - - def post(self, *args, **kwargs): - prepare_context = self.application.prepare_stage.configure() - - if prepare_context is not None: - configs = collections.defaultdict(lambda: dict()) - for name in self.request.body_arguments: - parsed = self.application.parse_form_name(prepare_context, name) - if parsed is not None: - (requirement, provider, unscoped_name) = parsed - value_strings = self.get_body_arguments(name) - value_string = value_strings[0] - values = configs[(requirement, provider)] - values[unscoped_name] = value_string - for ((requirement, provider), values) in configs.items(): - provider.set_config_values_as_strings( - requirement, prepare_context.environ, prepare_context.local_state_file, - prepare_context.default_env_spec_name, prepare_context.overrides, values) - - prepare_context.local_state_file.save() - - next_stage = self.application.prepare_stage.execute() - self.application.latest_statuses = self.application.prepare_stage.statuses_after_execute - if next_stage is None: - self.application.last_stage_result = self.application.prepare_stage.result - else: - self.application.latest_statuses = next_stage.statuses_before_execute - self.application.prepare_stage = next_stage - - return self.get(*args, **kwargs) - - -class UIApplication(Application): - def __init__(self, project, prepare_stage, event_handler, io_loop, **kwargs): - self._event_handler = event_handler - self.project = project - self.io_loop = io_loop - self.prepare_stage = prepare_stage - self.last_stage_result = None - self.latest_statuses = prepare_stage.statuses_before_execute - - self._requirements_by_id = {} - self._ids_by_requirement = {} - - patterns = [(r'/?', PrepareViewHandler)] - super(UIApplication, self).__init__(patterns, **kwargs) - - def emit_event(self, event): - self.io_loop.add_callback(lambda: self._event_handler(event)) - - def refresh_form_ids(self, prepare_context): - old_ids_by_requirement = self._ids_by_requirement - self._requirements_by_id = {} - self._ids_by_requirement = {} - for status in prepare_context.statuses: - req_id = old_ids_by_requirement.get(status.requirement, str(uuid.uuid4())) - self._requirements_by_id[req_id] = status.requirement - self._ids_by_requirement[status.requirement] = req_id - - def form_prefix(self, requirement, provider): - return "%s.%s." % (self._ids_by_requirement[requirement], provider.__class__.__name__) - - def parse_form_name(self, prepare_context, name): - pieces = name.split(".") - if len(pieces) < 3: - # this map on "pieces" is so py2 and py3 render it the same way, - # so the unit tests can be the same on both - print("not enough pieces in " + repr(list(map(lambda s: str(s), pieces))), file=sys.stderr) - return None - req_id = pieces[0] - provider_key = pieces[1] - unscoped_name = ".".join(pieces[2:]) - if req_id not in self._requirements_by_id: - print(req_id + " not a known requirement id", file=sys.stderr) - return None - requirement = self._requirements_by_id[req_id] - for status in prepare_context.statuses: - if status.requirement is requirement: - if provider_key == status.provider.__class__.__name__: - return (requirement, status.provider, unscoped_name) - print("did not find provider " + provider_key, file=sys.stderr) - return None - - -class UIServer(object): - def __init__(self, project, prepare_stage, event_handler, io_loop): - assert event_handler is not None - assert io_loop is not None - - self._application = UIApplication(project, prepare_stage, event_handler, io_loop) - self._http = HTTPServer(self._application, io_loop=io_loop) - - # these would throw OSError on failure - sockets = bind_sockets(port=None, address='127.0.0.1') - - self._port = None - for s in sockets: - # we have to find the ipv4 one - if s.family == socket.AF_INET: - self._port = s.getsockname()[1] - assert self._port is not None - - self._http.add_sockets(sockets) - self._http.start(1) - - @property - def port(self): - return self._port - - @property - def url(self): - return "http://localhost:%d/" % self.port - - def unlisten(self): - """Permanently close down the HTTP server, no longer listen on any sockets.""" - self._http.close_all_connections() - self._http.stop() diff --git a/anaconda_project/plugins/provider.py b/anaconda_project/plugins/provider.py index 6ccb0174..b2f26f98 100644 --- a/anaconda_project/plugins/provider.py +++ b/anaconda_project/plugins/provider.py @@ -265,12 +265,27 @@ def missing_env_vars_to_provide(self, requirement, environ, local_state_file): def read_config(self, requirement, environ, local_state_file, default_env_spec_name, overrides): """Read a config dict from the local state file for the given requirement. + You can think of this as the GET returning a web form for + configuring the provider. And in fact it was once used for + that, though we deleted the html stuff now. + + The returned 'config' has a 'source' field which was + essentially a selected radio option for where to get the + requirement, and other fields are entry boxes underneath + each radio option. + + This method still exists in the code in case we want to + do a textual version (or a new HTML version, but probably + outside of the anaconda-project codebase). See also + UI_MODE_TEXT_ASK_QUESTIONS in the cli code. + Args: requirement (Requirement): the requirement we're providing environ (dict): current environment variables local_state_file (LocalStateFile): file to read from default_env_spec_name (str): the fallback env spec name overrides (UserConfigOverrides): user-supplied forced config + """ pass # pragma: no cover @@ -278,6 +293,10 @@ def set_config_values_as_strings(self, requirement, environ, local_state_file, d values): """Set some config values in the state file (should not save the file). + You can think of this as the POST submitting a web form + for configuring the provider. And in fact it was once used + for that, though we deleted the html stuff now. + Args: requirement (Requirement): the requirement we're providing environ (dict): current environment variables @@ -285,37 +304,9 @@ def set_config_values_as_strings(self, requirement, environ, local_state_file, d default_env_spec_name (str): default env spec name for this prepare overrides (UserConfigOverrides): if any values in here change, delete the override values (dict): dict from string to string - """ - pass # silently ignore unknown config values - - def config_html(self, requirement, environ, local_state_file, overrides, status): - """Get an HTML string for configuring the provider. - - The HTML string must contain a single
tag. Any - ,