From b604ba92ecbb438e4eb06fc5181e648b9896b4a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9lio=20Guilherme?= Date: Fri, 25 Oct 2024 02:18:13 +0100 Subject: [PATCH 01/23] Cleanup code (#2905) * clean duplicates * clean duplicates * clean duplicates * clean duplicates * rename function * Refactor TextEdit, WIP * Fix unit tests with language transform * Fix insecure save of tmp file * Set Sonar for banches, update version * Set Sonar for banches, update version * Set Sonar for branches, update version * Separated key commands in TextEdit. Breaks some, TAB functions, Enter.. * Improve keys calls in TextEditor * Improve key use in TextEditor on Windows * Increase utest in TextEditor * Add requests to setup.py * First develop version --- .github/workflows/sonar.yml | 2 + setup.py | 3 +- sonar-project.properties | 1 + src/robotide/editor/dialoghelps.py | 32 +- src/robotide/editor/editordialogs.py | 63 ++- src/robotide/editor/kweditor.py | 133 +++-- src/robotide/editor/settingeditors.py | 2 +- src/robotide/editor/texteditor.py | 510 ++++++++++--------- src/robotide/lib/compat/parsing/languages.py | 27 +- src/robotide/preferences/__init__.py | 2 +- src/robotide/preferences/editors.py | 36 +- src/robotide/ui/__init__.py | 2 + src/robotide/ui/preferences_dialogs.py | 12 +- src/robotide/version.py | 2 +- utest/editor/test_texteditor.py | 51 +- utest/resources/fake.cfg | 25 +- 16 files changed, 502 insertions(+), 401 deletions(-) diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 803e12f03..abdd02af6 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -3,6 +3,8 @@ on: push: branches: - master + - develop + - cleanup_code jobs: sonarcloud: name: SonarCloud diff --git a/setup.py b/setup.py index f30051c84..ab44a5d79 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,8 @@ 'psutil', 'Pywin32; sys_platform=="win32"', 'wxPython', - 'packaging'] + 'packaging', + 'requests>=2.32.2'] PACKAGE_DATA = { 'robotide.preferences': ['settings.cfg'], diff --git a/sonar-project.properties b/sonar-project.properties index 2cec7f04b..577aed89c 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -2,6 +2,7 @@ sonar.projectKey=HelioGuilherme66_RIDE sonar.organization=helioguilherme66 sonar.host.url=https://sonarcloud.io sonar.python.version=3.10 +sonar.projectVersion=v2.1 sonar.sources=src/ sonar.tests=utest/ sonar.exclusions=**/lib/robot/**/* diff --git a/src/robotide/editor/dialoghelps.py b/src/robotide/editor/dialoghelps.py index 80486273e..f72b8a430 100644 --- a/src/robotide/editor/dialoghelps.py +++ b/src/robotide/editor/dialoghelps.py @@ -20,6 +20,10 @@ _ = GetTranslation # To keep linter/code analyser happy builtins.__dict__['_'] = GetTranslation +PH_ESCAPE ="%(ESCAPE)s" # PH = Place Holder +INHERIT_TAG = "Inherited tags are not shown in this view." +PH_TAGS = "%(TAG)s" +PH_FIXTURE = "%(FIXTURE)s" def get_help(title): _HELPS = {} @@ -46,44 +50,44 @@ def get_help(title): _("Give name, optional arguments and optional alias of the library to import."), _("Separate multiple arguments with a pipe character like 'arg 1 | arg 2'."), "%(ALIAS)s", '', "Variables", _("Give path and optional arguments of the variable file to import."), - _("Separate multiple arguments with a pipe character like 'arg 1 | arg 2'."), "%(ESCAPE)s", '', + _("Separate multiple arguments with a pipe character like 'arg 1 | arg 2'."), PH_ESCAPE, '', "Resource", _("Give path to the resource file to import."), _("Existing resources will be automatically loaded to the resource tree."), _("New resources must be created separately."), '', "Documentation", _("Give the documentation."), _("Simple formatting like *bold* and _italic_ can be used."), _("Additionally, URLs are converted to clickable links."), '', "Force Tags", _("These tags are set to all test cases in this test suite."), - _("Inherited tags are not shown in this view."), "%(TAG)s", "%(ESCAPE)s", '', "Default Tags", + _(INHERIT_TAG), PH_TAGS, PH_ESCAPE, '', "Default Tags", _("These tags are set to all test cases in this test suite unless test cases have their own tags."), - "%(TAG)s", "%(ESCAPE)s", '', "Test Tags", + PH_TAGS, PH_ESCAPE, '', "Test Tags", _("These tags are applied to all test cases in this test suite. " "This field exists since Robot Framework 6.0 and will replace " - "Force and Default Tags after version 7.0."), _("Inherited tags are not shown in this view."), - "%(TAG)s", "%(ESCAPE)s", '', "Tags", + "Force and Default Tags after version 7.0."), _(INHERIT_TAG), + PH_TAGS, PH_ESCAPE, '', "Tags", _("These tags are set to this test case in addition to " "Force Tags and they override possible Default Tags."), - _("Inherited tags are not shown in this view."), "%(TAG)s", "%(ESCAPE)s", '', "Suite Setup", + _(INHERIT_TAG), PH_TAGS, PH_ESCAPE, '', "Suite Setup", _("This keyword is executed before executing any of the test cases or lower level suites."), - "%(FIXTURE)s", "%(ESCAPE)s", '', "Suite Teardown", + PH_FIXTURE, PH_ESCAPE, '', "Suite Teardown", _("This keyword is executed after all test cases and lower level suites have been executed."), - "%(FIXTURE)s", "%(ESCAPE)s", '', "Test Setup", + PH_FIXTURE, PH_ESCAPE, '', "Test Setup", _("This keyword is executed before every test case in this suite unless test cases override it."), - "%(FIXTURE)s", "%(ESCAPE)s", '', "Test Teardown", + PH_FIXTURE, PH_ESCAPE, '', "Test Teardown", _("This keyword is executed after every test case in this suite unless test cases override it."), - "%(FIXTURE)s", "%(ESCAPE)s", '', "Setup", + PH_FIXTURE, PH_ESCAPE, '', "Setup", _("This keyword is executed before other keywords in this test case or keyword."), _("In test cases, overrides possible Test Setup set on the suite level."), - _("Setup in keywords exists since Robot v7.0."), "%(FIXTURE)s", "%(ESCAPE)s", '', "Teardown", + _("Setup in keywords exists since Robot v7.0."), PH_FIXTURE, PH_ESCAPE, '', "Teardown", _("This keyword is executed after other keywords in this test case or keyword even if the test or " "keyword fails."), _("In test cases, overrides possible Test Teardown set on the suite level."), - "%(FIXTURE)s", "%(ESCAPE)s", '', "Test Template", + PH_FIXTURE, PH_ESCAPE, '', "Test Template", _("Specifies the default template keyword used by tests in this suite."), _("The test cases will contain only data to use as arguments to that keyword."), '', "Template", _("Specifies the template keyword to use."), _("The test itself will contain only data to use as arguments to that keyword."), '', "Arguments", - "%(ARGUMENTS)s", "%(ESCAPE)s", '', "Return Value", - _("Specify the return value. Use a pipe character to separate multiple values."), "%(ESCAPE)s", + "%(ARGUMENTS)s", PH_ESCAPE, '', "Return Value", + _("Specify the return value. Use a pipe character to separate multiple values."), PH_ESCAPE, "The '[Return]' setting is deprecated since Robot v7.0. Use the 'RETURN' statement instead.", '', "Test Timeout", _("Maximum time test cases in this suite are allowed to execute before aborting them forcefully."), diff --git a/src/robotide/editor/editordialogs.py b/src/robotide/editor/editordialogs.py index 5805268bd..75c60c557 100644 --- a/src/robotide/editor/editordialogs.py +++ b/src/robotide/editor/editordialogs.py @@ -31,6 +31,15 @@ _ = wx.GetTranslation # To keep linter/code analyser happy builtins.__dict__['_'] = wx.GetTranslation +FORCE_TAGS = 'Force Tags' +DEFAULT_TAGS = 'Default Tags' +TEST_TAGS = 'Test Tags' +SUITE_SETUP = 'Suite Setup' +SUITE_TEAR = 'Suite Teardown' +TEST_SETUP = 'Test Setup' +TEST_TEAR = 'Test Teardown' +RET_VAL = 'Return Value' +TEST_TEMPL = 'Test Template' def editor_dialog(obj, lang='en'): set_lang = lang if lang and len(lang) > 0 else 'en' @@ -295,11 +304,11 @@ def _execute(self): class ForceTagsDialog(_SettingDialog): - _title_nt = 'Force Tags' + _title_nt = FORCE_TAGS - def __init__(self, controller, item=None, plugin=None, title=None, title_nt='Force Tags'): + def __init__(self, controller, item=None, plugin=None, title=None, title_nt=FORCE_TAGS): __ = title - self._title = _('Force Tags') + self._title = _(FORCE_TAGS) _SettingDialog.__init__(self, controller, item=item, plugin=plugin, title=self._title, title_nt=title_nt) def _execute(self): @@ -308,11 +317,11 @@ def _execute(self): class DefaultTagsDialog(_SettingDialog): - _title_nt = 'Default Tags' + _title_nt = DEFAULT_TAGS - def __init__(self, controller, item=None, plugin=None, title=None, title_nt='Default Tags'): + def __init__(self, controller, item=None, plugin=None, title=None, title_nt=DEFAULT_TAGS): __ = title - self._title = _('Default Tags') + self._title = _(DEFAULT_TAGS) _SettingDialog.__init__(self, controller, item=item, plugin=plugin, title=self._title, title_nt=title_nt) def _execute(self): @@ -321,11 +330,11 @@ def _execute(self): class TestTagsDialog(_SettingDialog): - _title_nt = 'Test Tags' + _title_nt = TEST_TAGS - def __init__(self, controller, item=None, plugin=None, title=None, title_nt='Test Tags'): + def __init__(self, controller, item=None, plugin=None, title=None, title_nt=TEST_TAGS): __ = title - self._title = _('Test Tags') + self._title = _(TEST_TAGS) _SettingDialog.__init__(self, controller, item=item, plugin=plugin, title=self._title, title_nt=title_nt) def _execute(self): @@ -367,11 +376,11 @@ def _execute(self): class SuiteSetupDialog(_FixtureDialog): tooltip = _("Suite Setup is run before any tests") - _title_nt = 'Suite Setup' + _title_nt = SUITE_SETUP - def __init__(self, controller, item=None, plugin=None, title=None, title_nt='Suite Setup'): + def __init__(self, controller, item=None, plugin=None, title=None, title_nt=SUITE_SETUP): __ = title - self._title = _('Suite Setup') + self._title = _(SUITE_SETUP) _FixtureDialog.__init__(self, controller, item=item, plugin=plugin, title=self._title, title_nt=title_nt) def _execute(self): @@ -380,11 +389,11 @@ def _execute(self): class SuiteTeardownDialog(_FixtureDialog): - _title_nt = 'Suite Teardown' + _title_nt = SUITE_TEAR - def __init__(self, controller, item=None, plugin=None, title=None, title_nt='Suite Teardown'): + def __init__(self, controller, item=None, plugin=None, title=None, title_nt=SUITE_TEAR): __ = title - self._title = _('Suite Teardown') + self._title = _(SUITE_TEAR) _FixtureDialog.__init__(self, controller, item=item, plugin=plugin, title=self._title, title_nt=title_nt) def _execute(self): @@ -394,11 +403,11 @@ def _execute(self): class TestSetupDialog(_FixtureDialog): __test__ = False - _title_nt = 'Test Setup' + _title_nt = TEST_SETUP - def __init__(self, controller, item=None, plugin=None, title=None, title_nt='Test Setup'): + def __init__(self, controller, item=None, plugin=None, title=None, title_nt=TEST_SETUP): __ = title - self._title = _('Test Setup') + self._title = _(TEST_SETUP) _FixtureDialog.__init__(self, controller, item=item, plugin=plugin, title=self._title, title_nt=title_nt) def _execute(self): @@ -408,11 +417,11 @@ def _execute(self): class TestTeardownDialog(_FixtureDialog): __test__ = False - _title_nt = 'Test Teardown' + _title_nt = TEST_TEAR - def __init__(self, controller, item=None, plugin=None, title=None, title_nt='Test Teardown'): + def __init__(self, controller, item=None, plugin=None, title=None, title_nt=TEST_TEAR): __ = title - self._title = _('Test Teardown') + self._title = _(TEST_TEAR) _FixtureDialog.__init__(self, controller, item=item, plugin=plugin, title=self._title, title_nt=title_nt) def _execute(self): @@ -463,11 +472,11 @@ def _execute(self): class TestTemplateDialog(_FixtureDialog): __test__ = False - _title_nt = 'Test Template' + _title_nt = TEST_TEMPL - def __init__(self, controller, item=None, plugin=None, title=None, title_nt='Test Template'): + def __init__(self, controller, item=None, plugin=None, title=None, title_nt=TEST_TEMPL): __ = title - self._title = _('Test Template') + self._title = _(TEST_TEMPL) _FixtureDialog.__init__(self, controller, item=item, plugin=plugin, title=self._title, title_nt=title_nt) def _execute(self): @@ -492,11 +501,11 @@ def _execute(self): class ReturnValueDialog(_SettingDialog): - _title_nt = 'Return Value' + _title_nt = RET_VAL - def __init__(self, controller, item=None, plugin=None, title=None, title_nt='Return Value'): + def __init__(self, controller, item=None, plugin=None, title=None, title_nt=RET_VAL): __ = title - self._title = _('Return Value') + self._title = _(RET_VAL) _SettingDialog.__init__(self, controller, item=item, plugin=plugin, title=self._title, title_nt=title_nt) def _execute(self): diff --git a/src/robotide/editor/kweditor.py b/src/robotide/editor/kweditor.py index d1f9dfc32..dcf66db8e 100755 --- a/src/robotide/editor/kweditor.py +++ b/src/robotide/editor/kweditor.py @@ -50,7 +50,17 @@ COL_HEADER_EDITOR = wx.NewId() PLUGIN_NAME = 'Editor' ZOOM_FACTOR = 'zoom factor' - +INS_ROWS = 'Insert Rows\tCtrl-I' +DEL_ROWS = 'Delete Rows\tCtrl-D' +CMT_CELLS = 'Comment Cells\tCtrl-Shift-3' +UCMT_CELLS = 'Uncomment Cells\tCtrl-Shift-4' +MV_CUR_DWN = 'Move Cursor Down\tAlt-Enter' +CMT_ROWS = 'Comment Rows\tCtrl-3' +UCMT_ROWS = 'Uncomment Rows\tCtrl-4' +MV_ROWS_UP = 'Move Rows Up\tAlt-Up' +MV_ROWS_DWN = 'Move Rows Down\tAlt-Down' +SWAP_ROWS_UP = 'Swap Row Up\tCtrl-T' +REN_KW = 'Rename Keyword' def requires_focus(function): def _row_header_selected_on_linux(self): @@ -84,7 +94,7 @@ def __init__(self, parent, controller, tree): parent.plugin.grid_popup_creator) self._popup_items = ([ _('Insert Cells\tCtrl-Shift-I'), _('Delete Cells\tCtrl-Shift-D'), - _('Insert Rows\tCtrl-I'), _('Delete Rows\tCtrl-D'), '---', + _(INS_ROWS), _(DEL_ROWS), '---', _('Select All\tCtrl-A'), '---', _('Cut\tCtrl-X'), _('Copy\tCtrl-C'), _('Paste\tCtrl-V'), _('Insert\tCtrl-Shift-V'), '---', _('Delete\tDel'), '---'] + @@ -92,7 +102,7 @@ def __init__(self, parent, controller, tree): _('Create Keyword'), _('Extract Keyword'), _('Extract Variable'), - _('Rename Keyword'), + _(REN_KW), _('Find Where Used'), _('JSON Editor\tCtrl-Shift-J'), '---', @@ -105,19 +115,19 @@ def __init__(self, parent, controller, tree): _('Make List Variable\tCtrl-2'), _('Make Dict Variable\tCtrl-5'), '---', - _('Comment Cells\tCtrl-Shift-3'), - _('Uncomment Cells\tCtrl-Shift-4'), - _('Move Cursor Down\tAlt-Enter'), + _(CMT_CELLS), + _(UCMT_CELLS), + _(MV_CUR_DWN), '---', - _('Comment Rows\tCtrl-3'), - _('Uncomment Rows\tCtrl-4'), - _('Move Rows Up\tAlt-Up'), - _('Move Rows Down\tAlt-Down'), - _('Swap Row Up\tCtrl-T') + _(CMT_ROWS), + _(UCMT_ROWS), + _(MV_ROWS_UP), + _(MV_ROWS_DWN), + _(SWAP_ROWS_UP) ]) self._popup_items_nt = ([ 'Insert Cells\tCtrl-Shift-I', 'Delete Cells\tCtrl-Shift-D', - 'Insert Rows\tCtrl-I', 'Delete Rows\tCtrl-D', '---', + INS_ROWS, DEL_ROWS, '---', 'Select All\tCtrl-A', '---', 'Cut\tCtrl-X', 'Copy\tCtrl-C', 'Paste\tCtrl-V', 'Insert\tCtrl-Shift-V', '---', 'Delete\tDel', '---'] + @@ -125,7 +135,7 @@ def __init__(self, parent, controller, tree): 'Create Keyword', 'Extract Keyword', 'Extract Variable', - 'Rename Keyword', + REN_KW, 'Find Where Used', 'JSON Editor\tCtrl-Shift-J', '---', @@ -138,15 +148,15 @@ def __init__(self, parent, controller, tree): 'Make List Variable\tCtrl-2', 'Make Dict Variable\tCtrl-5', '---', - 'Comment Cells\tCtrl-Shift-3', - 'Uncomment Cells\tCtrl-Shift-4', - 'Move Cursor Down\tAlt-Enter', + CMT_CELLS, + UCMT_CELLS, + MV_CUR_DWN, '---', - 'Comment Rows\tCtrl-3', - 'Uncomment Rows\tCtrl-4', - 'Move Rows Up\tAlt-Up', - 'Move Rows Down\tAlt-Down', - 'Swap Row Up\tCtrl-T' + CMT_ROWS, + UCMT_ROWS, + MV_ROWS_UP, + MV_ROWS_DWN, + SWAP_ROWS_UP ]) self._parent = parent self._plugin = parent.plugin @@ -314,13 +324,6 @@ def on_select_cell(self, event): event.Skip() def on_kill_focus(self, event): - # if self.col_label_element: - # try: - # self.col_label_element[0].Destroy() - # except RuntimeError: - # print("DEBUG: on_kill_focus exception called") - # finally: - # del self.col_label_element self._tooltips.hide() self._hide_link_if_necessary() event.Skip() @@ -350,28 +353,28 @@ def _row_label_right_click(self, event): self.SelectRow(selected_row, addToSelected=False) self.SetGridCursor(event.Row, 0) popupitems = [ - _('Insert Rows\tCtrl-I'), - _('Delete Rows\tCtrl-D'), - _('Comment Rows\tCtrl-3'), - _('Uncomment Rows\tCtrl-4'), - _('Move Rows Up\tAlt-Up'), - _('Move Rows Down\tAlt-Down'), - _('Swap Row Up\tCtrl-T'), + _(INS_ROWS), + _(DEL_ROWS), + _(CMT_ROWS), + _(UCMT_ROWS), + _(MV_ROWS_UP), + _(MV_ROWS_DWN), + _(SWAP_ROWS_UP), '---', - _('Comment Cells\tCtrl-Shift-3'), - _('Uncomment Cells\tCtrl-Shift-4'), + _(CMT_CELLS), + _(UCMT_CELLS), ] popupitems_nt = [ - 'Insert Rows\tCtrl-I', - 'Delete Rows\tCtrl-D', - 'Comment Rows\tCtrl-3', - 'Uncomment Rows\tCtrl-4', - 'Move Rows Up\tAlt-Up', - 'Move Rows Down\tAlt-Down', - 'Swap Row Up\tCtrl-T', + INS_ROWS, + DEL_ROWS, + CMT_ROWS, + UCMT_ROWS, + MV_ROWS_UP, + MV_ROWS_DWN, + SWAP_ROWS_UP, '---', - 'Comment Cells\tCtrl-Shift-3', - 'Uncomment Cells\tCtrl-Shift-4', + CMT_CELLS, + UCMT_CELLS, ] PopupMenu(self, PopupMenuItems(self, popupitems, popupitems_nt)) event.Skip() @@ -407,7 +410,6 @@ def on_col_label_edit(self, event: wx.KeyEvent): if keycode == wx.WXK_ESCAPE: wx.CallAfter(edit.Destroy) if keycode == wx.WXK_RETURN: - # element = event.GetPosition() value = edit.GetValue() if value == '': del self._controller.data.parent.header[col+1] @@ -652,8 +654,6 @@ def on_copy(self, event=None): # DEBUG @requires_focus def on_cut(self, event=None): - # self._clipboard_handler.cut() - # print(f"DEBUG: kweditor.py on_cut called event {str(event)}") self.cut() self.on_delete(event) @@ -680,19 +680,22 @@ def _execute_clipboard_command(self, command_class): if isinstance(data, str): data = [[self._string_to_cell(data)]] elif isinstance(data, list) and isinstance(data[0], list): - main_data = [] - for ldata in data: - new_data = [] - for rdata in ldata: - sdata = self._string_to_cell(rdata) - new_data.append(sdata) - main_data.append(new_data) - data = main_data + data = self._get_main_data(data) self._execute(command_class(self.selection.topleft, data)) + def _get_main_data(self, data: []) -> []: + main_data = [] + for ldata in data: + new_data = [] + for rdata in ldata: + sdata = self._string_to_cell(rdata) + new_data.append(sdata) + main_data.append(new_data) + return main_data + def _string_to_cell(self, content: str) -> str: spaces = ' ' * self._spacing - cells = content.replace(' | ', spaces).replace(spaces, '\t').strip() # TODO: Make this cells + cells = content.replace(' | ', spaces).replace(spaces, '\t').strip() # DEBUG: Make this cells return cells # DEBUG @@ -789,8 +792,6 @@ def _call_ctrl_function(self, event: object, keycode: int): elif keycode == ord('C'): self.on_copy(event) elif keycode == ord('X'): - # print("DEBUG: kweditor.py _call_ctrl_function Pressed CTRL-X") - # self.on_cut(event) return False elif keycode == ord('V'): self.on_paste(event) @@ -843,14 +844,8 @@ def _call_alt_function(self, event, keycode: int): return False # event must not be skipped in this case return True - """ - elif keycode in [wx.WXK_DOWN, wx.WXK_UP]: - print(f"DEBUG kweditor call move_rews ky={keycode}") - # Mac Option key(⌥) - self._move_rows(keycode) - """ - def on_key_down(self, event): + # print(f"DEBUG: KeywordEditor on_key_down event={event} focus={self.is_focused()}") keycode = event.GetUnicodeKey() or event.GetKeyCode() if event.ControlDown(): if event.ShiftDown(): @@ -865,6 +860,7 @@ def on_key_down(self, event): event.Skip() def on_char(self, event): + # print(f"DEBUG: KeywordEditor on_char event={event} focus={self.is_focused()}") key_char = event.GetUnicodeKey() if key_char < ord(' '): return @@ -954,6 +950,7 @@ def move_grid_cursor_and_edit(self): self.open_cell_editor() def on_key_up(self, event): + # print(f"DEBUG: KeywordEditor on_key_up event={event} focus={self.is_focused()}") event.Skip() # DEBUG seen this skip as soon as possible self._tooltips.hide() self._hide_link_if_necessary() @@ -1153,7 +1150,7 @@ def on_rename_keyword(self, event): old_name = self._current_cell_value() if not old_name.strip() or variablematcher.is_variable(old_name): return - new_name = wx.GetTextFromUser(_('New name'), _('Rename Keyword'), default_value=old_name) + new_name = wx.GetTextFromUser(_('New name'), _(REN_KW), default_value=old_name) if new_name: self._execute(RenameKeywordOccurrences( old_name, new_name, RenameProgressObserver(self.GetParent()))) diff --git a/src/robotide/editor/settingeditors.py b/src/robotide/editor/settingeditors.py index 976bd48bb..0605ee07d 100755 --- a/src/robotide/editor/settingeditors.py +++ b/src/robotide/editor/settingeditors.py @@ -140,7 +140,7 @@ def on_display_motion(self, event): except AttributeError: pass - def refresh(self, controller): + def refresh_values(self, controller): self._controller = controller self.update_value() diff --git a/src/robotide/editor/texteditor.py b/src/robotide/editor/texteditor.py index d4c7f3ee9..1071d2042 100644 --- a/src/robotide/editor/texteditor.py +++ b/src/robotide/editor/texteditor.py @@ -15,6 +15,7 @@ import builtins import os import re +import tempfile from io import StringIO, BytesIO from os.path import dirname from time import time @@ -65,6 +66,7 @@ TOKEN_TXT = 'Token(' TXT_NUM_SPACES = 'txt number of spaces' ZOOM_FACTOR = 'zoom factor' +RSPC = r"\s{2}" def read_language(content): @@ -88,17 +90,11 @@ def obtain_language(existing, content): except AttributeError: # Unittests fails here set_lang = [] doc_lang = read_language(content) - # print(f"DEBUG: textedit.py validate_and_update EMTER obtain_language={doc_lang}") adoc_lang = [] if doc_lang is not None: if isinstance(doc_lang, str): adoc_lang.append(doc_lang) - for idx, lang in enumerate(adoc_lang): - try: - mlang = Language.from_name(lang.replace('_', '-').strip()) - except ValueError as e: - raise e - set_lang[idx] = get_rf_lang_code(mlang.code) # .code.replace('-','_') + set_lang = _get_lang(set_lang, adoc_lang) elif len(set_lang) > 0: if existing is not None: if isinstance(existing, list): @@ -112,10 +108,20 @@ def obtain_language(existing, content): set_lang[0] = 'en' else: set_lang[0] = 'en' - # print(f"DEBUG: textedit.py validate_and_update obtain_language RETURN ={[set_lang[0]]}") return [set_lang[0]] +def _get_lang(set_lang:list, adoc_lang: list) -> list: + for idx, lang in enumerate(adoc_lang): + try: + mlang = Language.from_name(lang.replace('_', '-').strip()) + except ValueError as e: + print(f"DEBUG: TextEditor, could not find Language:{lang}") + raise e + set_lang[idx] = get_rf_lang_code(mlang.code) # .code.replace('-','_') + return set_lang + + def get_rf_lang_code(lang: (str, list), iso: bool=False) -> str: if isinstance(lang, list): clean_lang = lang @@ -132,28 +138,25 @@ def get_rf_lang_code(lang: (str, list), iso: bool=False) -> str: if with_variant_code in ("PtBr", "ZhCn", "ZhTw") and not iso: return with_variant_code if iso: - variant = {"bs":"BA", "cs":"CZ", "da":"DK", "en":"US", "hi":"IN", "ja":"JP", - "ko":"KR", "sv":"SE", "uk":"UA", "vi":"VN"} - code = clean_lang[0].lower() - if lc == 1: - if code in variant.keys(): - return f"{code}_{variant[code].upper()}" - else: - return f"{code}_{code.upper()}" - else: - return f"{code}_{clean_lang[1].upper()}" + return _four_letters_code(clean_lang) return code.title() -def transform_doc_language(old_lang, new_lang, m_text, node_info: tuple = ('', )): - if isinstance(old_lang, list): - old_lang = old_lang[0] - if isinstance(new_lang, list): - new_lang = new_lang[0] - old_lang = old_lang.title() - new_lang = new_lang.title() - if old_lang == new_lang: - return m_text +def _four_letters_code(clean_lang: list) -> str: + variant = {"bs": "BA", "cs": "CZ", "da": "DK", "en": "US", "hi": "IN", "ja": "JP", + "ko": "KR", "sv": "SE", "uk": "UA", "vi": "VN"} + lc = len(clean_lang) + code = clean_lang[0].lower() + if lc == 1: + if code in variant.keys(): + return f"{code}_{variant[code].upper()}" + else: + return f"{code}_{code.upper()}" + else: + return f"{code}_{clean_lang[1].upper()}" + + +def _get_lang_classes(old_lang: str, new_lang: str) -> (Language, Language): try: old_lang_class = Language.from_name(old_lang) except ValueError as ex: @@ -164,6 +167,49 @@ def transform_doc_language(old_lang, new_lang, m_text, node_info: tuple = ('', ) except ValueError as ex: print(ex) new_lang_class = Language.from_name('English') + return old_lang_class, new_lang_class + + +def _check_lang_error(node_info: tuple, m_text) -> (bool, str): + signal_correct_language = False + if node_info != ('', ) and node_info[0] == 'ERROR': + c_msg = node_info[1].replace(TOKEN_TXT, '').replace(')', '').split(',') + line = c_msg[1].replace('\'', '').strip() + # print(f"DEBUG: textedit.py transform_doc_language ERROR:{line}") + if line.startswith(LANG_SETTING): + tail = line.replace(LANG_SETTING, '') + # print(f"DEBUG: textedit.py transform_doc_language INSIDE BLOCK {tail=}") + m_text = m_text.replace(LANG_SETTING + tail, LANG_SETTING + 'English' + ' # ' + tail) + signal_correct_language = True + return signal_correct_language, m_text + + +def _final_lang_transformation(signal_correct_language: bool, old_lang_name: str, new_lang_name: str, m_text: str) -> str: + if signal_correct_language: + m_text = m_text.replace(fr'{LANG_SETTING}English', fr'{LANG_SETTING}{new_lang_name}') + else: + m_text = m_text.replace(fr'{LANG_SETTING}{old_lang_name}', fr'{LANG_SETTING}{new_lang_name}') + try: + set_lang = shared_memory.ShareableList(name="language") + except AttributeError: # Unittests fails here + set_lang = [] + try: + mlang = Language.from_name(new_lang_name.replace('_', '-')) + set_lang[0] = get_rf_lang_code(mlang.code) + except ValueError: + set_lang[0] = 'en' + return m_text + +def transform_doc_language(old_lang, new_lang, m_text, node_info: tuple = ('', )): + if isinstance(old_lang, list): + old_lang = old_lang[0] + if isinstance(new_lang, list): + new_lang = new_lang[0] + old_lang = old_lang.title() + new_lang = new_lang.title() + if old_lang == new_lang: + return m_text + old_lang_class, new_lang_class = _get_lang_classes(old_lang, new_lang) old_lang_name = old_lang_class.name new_lang_name = new_lang_class.name if old_lang_name == new_lang_name: @@ -234,35 +280,8 @@ def transform_doc_language(old_lang, new_lang, m_text, node_info: tuple = ('', ) old_false_strings = old_lang_class.false_strings new_true_strings = new_lang_class.true_strings new_false_strings = new_lang_class.false_strings - sinal_correct_language = False # If error in Language, do final replacement - if node_info != ('', ): - if node_info[0] == 'ERROR': - c_msg = node_info[1].replace(TOKEN_TXT, '').replace(')', '').split(',') - line = c_msg[1].replace('\'', '').strip() - # print(f"DEBUG: textedit.py transform_doc_language ERROR:{line}") - if line.startswith(LANG_SETTING): - tail = line.replace(LANG_SETTING, '') - # print(f"DEBUG: textedit.py transform_doc_language INSIDE BLOCK {tail=}") - m_text = m_text.replace(LANG_SETTING + tail, LANG_SETTING + 'English' + ' # ' + tail) - sinal_correct_language = True - """ - if node_info[0] == 'INVALID_HEADER': - # print(f"DEBUG: textedit.py transform_doc_language INVALID_HEADER: {node_info[1]}") - old_header = node_info[1].split(',')[1] - old_header = old_header.replace('* ', '').replace(' *', '').replace('*', '').strip('\' ') - headers = list(old_lang_headers.keys()) - try: - idx = headers.index(old_header) - except ValueError: - # print(f"DEBUG: language.py get_english_label Exception at getting index {old_lang} returning= m_text") - return m_text - en_label = list(old_lang_headers.values())[idx] - new_header = list(new_lang_headers.keys())[idx] - print(f"DEBUG: textedit.py transform_doc_language OLD_HEADER: {old_header} en_label={en_label} " - f"{new_header=}") - m_text = m_text.replace(old_header, new_header) - """ - + # If error in Language, do final replacement + signal_correct_language, m_text = _check_lang_error(node_info, m_text) for old, new in zip(old_lang_headers.keys(), new_lang_headers.keys()): m_text = re.sub(r"[*]+\s"+fr"{old}"+r"\s[*]+", fr"*** {new} ***", m_text) # Settings must be replaced individually @@ -303,35 +322,20 @@ def transform_doc_language(old_lang, new_lang, m_text, node_info: tuple = ('', ) m_text = re.sub(fr'^{old_name_setting}\b', fr'{new_name_setting}', m_text, flags=re.M) for old, new in zip(old_lang_given_prefixes, new_lang_given_prefixes): - m_text = re.sub(r"\s{2}"+fr"{old}"+r"\s", fr" {new} ", m_text) + m_text = re.sub(RSPC+fr"{old}"+r"\s", fr" {new} ", m_text) for old, new in zip(old_lang_when_prefixes, new_lang_when_prefixes): - m_text = re.sub(r"\s{2}"+fr"{old}"+r"\s", fr" {new} ", m_text) + m_text = re.sub(RSPC+fr"{old}"+r"\s", fr" {new} ", m_text) for old, new in zip(old_lang_then_prefixes, new_lang_then_prefixes): - m_text = re.sub(r"\s{2}"+fr"{old}"+r"\s", fr" {new} ", m_text) + m_text = re.sub(RSPC+fr"{old}"+r"\s", fr" {new} ", m_text) for old, new in zip(old_lang_and_prefixes, new_lang_and_prefixes): - m_text = re.sub(r"\s{2}"+fr"{old}"+r"\s", fr" {new} ", m_text) + m_text = re.sub(RSPC+fr"{old}"+r"\s", fr" {new} ", m_text) for old, new in zip(old_lang_but_prefixes, new_lang_but_prefixes): - m_text = re.sub(r"\s{2}"+fr"{old}"+r"\s", fr" {new} ", m_text) + m_text = re.sub(RSPC+fr"{old}"+r"\s", fr" {new} ", m_text) # Before ending, we replace broken keywords from excluded known bad tanslations m_text = transform_standard_keywords(new_lang_name, m_text) - # print(f"DEBUG: texteditor.py transform_doc_language {m_text=}") + return _final_lang_transformation(signal_correct_language, old_lang_name, new_lang_name, m_text) - if sinal_correct_language: - m_text = m_text.replace(fr'{LANG_SETTING}English', fr'{LANG_SETTING}{new_lang_name}') - else: - m_text = m_text.replace(fr'{LANG_SETTING}{old_lang_name}', fr'{LANG_SETTING}{new_lang_name}') - - try: - set_lang = shared_memory.ShareableList(name="language") - except AttributeError: # Unittests fails here - set_lang = [] - try: - mlang = Language.from_name(new_lang_name.replace('_', '-')) - set_lang[0] = get_rf_lang_code(mlang.code) - except ValueError: - set_lang[0] = 'en' - return m_text def transform_standard_keywords(new_lang: str, content: str) -> str: """ @@ -348,9 +352,9 @@ def transform_standard_keywords(new_lang: str, content: str) -> str: return content path_to_exclusion = f"{PATH_EXCLUSIONS}/../localization/{lang_code}/restore_keywords.json" - # print(f"DEBUG: texteditor.py transform_standard_keywords path={path_to_exclusion}\n" - # f"{lang_code=}\n" - # f"{mlang.code=}") + print(f"DEBUG: texteditor.py transform_standard_keywords path={path_to_exclusion}\n" + f"{lang_code=}\n" + f"{mlang.code=}") import json try: @@ -477,6 +481,31 @@ def _set_shared_doc_lang(self, lang='en'): set_lang = [] self._doc_language = set_lang[0] = lang + def _check_message(self, message: RideMessage) -> None: + if isinstance(message, RideOpenSuite): # Not reached + self._editor.reset() + self._editor.set_editor_caret_position() + if isinstance(message, RideNotebookTabChanging): # Not reached + return + # Workaround for remarked dirty with Ctrl-S + if self.is_focused() and self._save_flag == 0 and isinstance(message, RideSaving): + self._save_flag = 1 + RideBeforeSaving().publish() + if self.is_focused() and self._save_flag == 1 and isinstance(message, RideDataDirtyCleared): + self._save_flag = 2 + if self.is_focused() and self._save_flag == 2 and isinstance(message, RideSaved): + self._save_flag = 3 + wx.CallAfter(self._editor.mark_file_dirty, False) + # DEBUG: This is the unwanted chnge after saving but excluded in this block for performance + # if self.is_focused() and self._save_flag == 3 and isinstance(message, RideDataChangedToDirty): + # self._save_flag = 4 + # wx.CallAfter(self._editor.mark_file_dirty, False) + if isinstance(message, RideBeforeSaving): + self._editor.is_saving = False + # Reset counter for Workaround for remarked dirty with Ctrl-S + self._save_flag = 0 + self._apply_txt_changes_to_model() + def on_data_changed(self, message): """ This block is now inside try/except to avoid errors from unit test """ try: @@ -485,29 +514,7 @@ def on_data_changed(self, message): return if self._should_process_data_changed_message(message): # print(f"DEBUG: textedit after _should_process_data_changed_message save_flag={self._save_flag}") - if isinstance(message, RideOpenSuite): # Not reached - self._editor.reset() - self._editor.set_editor_caret_position() - if isinstance(message, RideNotebookTabChanging): # Not reached - return - # Workaround for remarked dirty with Ctrl-S - if self.is_focused() and self._save_flag == 0 and isinstance(message, RideSaving): - self._save_flag = 1 - RideBeforeSaving().publish() - if self.is_focused() and self._save_flag == 1 and isinstance(message, RideDataDirtyCleared): - self._save_flag = 2 - if self.is_focused() and self._save_flag == 2 and isinstance(message, RideSaved): - self._save_flag = 3 - wx.CallAfter(self._editor.mark_file_dirty, False) - # DEBUG: This is the unwanted chnge after saving but excluded in this block for performance - # if self.is_focused() and self._save_flag == 3 and isinstance(message, RideDataChangedToDirty): - # self._save_flag = 4 - # wx.CallAfter(self._editor.mark_file_dirty, False) - if isinstance(message, RideBeforeSaving): - self._editor.is_saving = False - # Reset counter for Workaround for remarked dirty with Ctrl-S - self._save_flag = 0 - self._apply_txt_changes_to_model() + self._check_message(message) self._refresh_timer.Start(500, True) # For performance reasons only run after all the data changes except AttributeError: @@ -637,6 +644,9 @@ def __init__(self, plugin, lang=None): self._doc_language = lang else: self._get_shared_doc_lang() + file = tempfile.NamedTemporaryFile(prefix="model_saved_from_RIDE_", + suffix=".robot", mode="w+", delete=False) + self.tempfilename=file.name def _get_shared_doc_lang(self): try: @@ -749,7 +759,7 @@ def _sanity_check(self, data, text): validator.visit(model) except DataError as err: result = (err.message, err.details) - model.save("/tmp/model_saved_from_RIDE.robot") + model.save(self.tempfilename) # print(f"DEBUG: textedit.py _sanity_check after calling validator {validator}\n" # f"Save model in /tmp/model_saved_from_RIDE.robot" # f" result={result}") @@ -1352,6 +1362,7 @@ def indent_line(self, line, reverse=False): self.source_editor.DeleteRange(pos, self.tab_size) def indent_block(self): + # print(f"DEBUG: TextEditor SourceEdior ident_block focus={self.is_focused()}") start, end = self.source_editor.GetSelection() ini_line = self.source_editor.LineFromPosition(start) end_line = self.source_editor.LineFromPosition(end) @@ -1363,6 +1374,7 @@ def indent_block(self): self.source_editor.SetSelection(pos, pos) self.source_editor.SetInsertionPoint(pos) self.write_ident() + # print(f"DEBUG: TextEditor SourceEdior ident_block loop line={line}") line += 1 tnew_start = self.source_editor.GetLineEndPosition(ini_line) - len(self.source_editor.GetLine(ini_line)) + 1 tnew_end = self.source_editor.GetLineEndPosition(end_line) @@ -1554,12 +1566,12 @@ def revert(self): # self.source_editor.set_text(self._data.content) def on_editor_key(self, event): + # print(f"DEBUG: TextEditor on_editor_key event={event} focus={self.is_focused()}") if not self.is_focused(): event.Skip() return keycode = event.GetKeyCode() keyvalue = event.GetUnicodeKey() - # print(f"DEBUG: TextEditor key up focused={self.is_focused()} modify {self.source_editor.GetModify()}") if keycode in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER]: self.mark_file_dirty(self.source_editor.GetModify()) return @@ -1587,6 +1599,7 @@ def on_key_down(self, event): :param event: :return: """ + # print(f"DEBUG: TextEditor on_key_down event={event} focus={self.is_focused()}") if not self.is_focused(): event.Skip() return @@ -1595,9 +1608,6 @@ def on_key_down(self, event): # print(f"DEBUG: TextEditor on_key_down event={event} raw_key={raw_key} wx.WXK_C ={wx.WXK_CONTROL}") if event.GetKeyCode() == wx.WXK_DELETE: self.mark_file_dirty(self.source_editor.GetModify()) - # print(f"DEBUG: TextEditor on_key_down event={event} raw_key={raw_key} wx.WXK_C ={wx.WXK_CONTROL}" - # f"\n KEY=DELETE DIRTY:{self.dirty}") - event.Skip() return if raw_key != wx.WXK_CONTROL: # We need to clear doc as soon as possible self.source_editor.hide_kw_doc() @@ -1614,6 +1624,7 @@ def on_key_down(self, event): else: self.indent_block() self.mark_file_dirty(self.source_editor.GetModify()) + return elif event.GetKeyCode() == wx.WXK_TAB and event.ShiftDown(): selected = self.source_editor.GetSelection() if selected[0] == selected[1]: @@ -1626,6 +1637,7 @@ def on_key_down(self, event): else: self.deindent_block() self.mark_file_dirty(self.source_editor.GetModify()) + return elif event.GetKeyCode() in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER]: if not self._showing_list: self.auto_indent() @@ -1634,14 +1646,16 @@ def on_key_down(self, event): wx.CallAfter(self.write_ident) # DEBUG: Make this configurable? event.Skip() self.mark_file_dirty(self.source_editor.GetModify()) + return elif keycode in (ord('1'), ord('2'), ord('5')) and event.ControlDown(): self.execute_variable_creator(list_variable=(keycode == ord('2')), dict_variable=(keycode == ord('5'))) self.store_position() self.mark_file_dirty(self.source_editor.GetModify()) - elif (not IS_WINDOWS and not IS_MAC and keycode in (ord('v'), ord('V')) - and event.ControlDown() and not event.ShiftDown()): + elif ((not IS_WINDOWS and not IS_MAC and keycode in (ord('v'), ord('V')) + or keycode in (ord('d'), ord('D'))) and event.ControlDown() and not event.ShiftDown()): # We need to ignore this in Linux, because it does double-action + # We need to ignore Ctl-D because Scintilla does Duplicate line self.mark_file_dirty(self.source_editor.GetModify()) return elif keycode in (ord('g'), ord('G')) and event.ControlDown(): @@ -1650,17 +1664,11 @@ def on_key_down(self, event): else: wx.CallAfter(self.on_find, event) return - elif keycode in (ord('d'), ord('D')) and event.ControlDown() and not event.ShiftDown(): - # We need to ignore because Scintilla does Duplicate line - self.mark_file_dirty(self.source_editor.GetModify()) - return elif event.ControlDown() and raw_key == wx.WXK_CONTROL: # This must be the last branch to activate actions before doc # DEBUG: coords = self._get_screen_coordinates() self.source_editor.show_kw_doc() - event.Skip() - else: - event.Skip() + event.Skip() # These commands are duplicated by global actions """ DEBUG @@ -1769,105 +1777,116 @@ def _enclose_text(open_symbol, value=''): close_symbol = open_symbol return open_symbol + value + close_symbol - def move_row_up(self, event): - __ = event - start, end = self.source_editor.GetSelection() - new_ini_line = ini_line = self.source_editor.LineFromPosition(start) + def _prepare_selection(self, start, end): new_end_line = end_line = self.source_editor.LineFromPosition(end) last_line = self.source_editor.GetLineCount() - # selection not on top? - if ini_line > 0: - # get the next row content - if end_line + 1 < last_line - 3: - rowbelow = self.source_editor.GetLine(end_line + 1) - # exception if moving block is long assignemnt or arguments with continuation markers extend selection - is_marker = self.first_non_space_content(rowbelow) - if is_marker == '...': - new_end_line = self.find_initial_keyword(end_line, up=False) - if new_end_line != end_line: # Extend selection - new_end_pos = self.source_editor.GetLineEndPosition(new_end_line - 1) - self.source_editor.SetSelection(start, new_end_pos-1) - # exception if is long assignemnt or arguments with continuation markers get top line - # get the previous row content - rowabove = self.source_editor.GetLine(ini_line - 1) - is_marker = self.first_non_space_content(rowabove) + # get the next row content + if end_line + 1 < last_line - 3: + rowbelow = self.source_editor.GetLine(end_line + 1) + # exception if moving block is long assignemnt or arguments with continuation markers extend selection + is_marker = self.first_non_space_content(rowbelow) if is_marker == '...': - new_ini_line = max(1, self.find_initial_keyword(ini_line)) - self.source_editor.BeginUndoAction() - if new_ini_line != ini_line: - # Move block to up new_ini_line - delta = ini_line - new_ini_line - for _ in range(0, delta): - self.source_editor.MoveSelectedLinesUp() - else: + new_end_line = self.find_initial_keyword(end_line, up=False) + if new_end_line != end_line: # Extend selection + new_end_pos = self.source_editor.GetLineEndPosition(new_end_line - 1) + self.source_editor.SetSelection(start, new_end_pos - 1) + + def _above_row_selection(self, new_ini_line, ini_line): + # get the previous row content + rowabove = self.source_editor.GetLine(ini_line - 1) + is_marker = self.first_non_space_content(rowabove) + if is_marker == '...': + new_ini_line = max(1, self.find_initial_keyword(ini_line)) + self.source_editor.BeginUndoAction() + if new_ini_line != ini_line: + # Move block to up new_ini_line + delta = ini_line - new_ini_line + for _ in range(0, delta): self.source_editor.MoveSelectedLinesUp() - self.source_editor.EndUndoAction() - # New selection - start, end = self.source_editor.GetSelection() - new_end_line = self.source_editor.LineFromPosition(end - 1) # One char before - nendpos = self.source_editor.GetLineEndPosition(new_end_line) - self.source_editor.SetAnchor(nendpos) - new_ini_line = self.source_editor.LineFromPosition(start) - # indentation after move - new_top = self.source_editor.GetLine(new_ini_line) - rowbelow = self.source_editor.GetLine(new_end_line + 1) - old_start = self.first_non_space(rowbelow) - new_start = self.first_non_space(new_top) - was_end_old = rowbelow[old_start:old_start+3] == 'END' - was_end_new = new_top[new_start:new_start+3] == 'END' - if new_start == old_start and was_end_old and was_end_new: - self.deindent_line(new_end_line + 1) - self.indent_block() - elif new_start == old_start and was_end_old: - self.indent_block() - start, end = self.source_editor.GetSelection() - if old_start > new_start and was_end_new: - self.deindent_line(new_end_line + 1) - if was_end_old: - self.indent_block() - if new_start < old_start and not was_end_new: + else: + self.source_editor.MoveSelectedLinesUp() + + def move_row_up(self, event): + __ = event + start, end = self.source_editor.GetSelection() + new_ini_line = ini_line = self.source_editor.LineFromPosition(start) + if ini_line == 0: + return + self._prepare_selection(start, end) + # exception if is long assignemnt or arguments with continuation markers get top line + self._above_row_selection(new_ini_line, ini_line) + self.source_editor.EndUndoAction() + # New selection + start, end = self.source_editor.GetSelection() + new_end_line = self.source_editor.LineFromPosition(end - 1) # One char before + nendpos = self.source_editor.GetLineEndPosition(new_end_line) + self.source_editor.SetAnchor(nendpos) + new_ini_line = self.source_editor.LineFromPosition(start) + # indentation after move + new_top = self.source_editor.GetLine(new_ini_line) + rowbelow = self.source_editor.GetLine(new_end_line + 1) + old_start = self.first_non_space(rowbelow) + new_start = self.first_non_space(new_top) + was_end_old = rowbelow[old_start:old_start+3] == 'END' + was_end_new = new_top[new_start:new_start+3] == 'END' + if new_start == old_start and was_end_old and was_end_new: + self.deindent_line(new_end_line + 1) + self.indent_block() + elif new_start == old_start and was_end_old: + self.indent_block() + start, end = self.source_editor.GetSelection() + if old_start > new_start and was_end_new: + self.deindent_line(new_end_line + 1) + if was_end_old: self.indent_block() - elif new_start > old_start: - self.deindent_block() + if new_start < old_start and not was_end_new: + self.indent_block() + elif new_start > old_start: + self.deindent_block() + + def _find_up_kw(self, start:int) -> int: + if start <= 3: + return start + text = self.source_editor.GetLine(start) + is_marker = self.first_non_space_content(text) # check if selection starts with marker + if is_marker == '...': + return start + is_marker = '...' + found = False + line = start - 2 + while is_marker == '...' and line > 2: + row = self.source_editor.GetLine(line) + is_marker = self.first_non_space_content(row) + if is_marker != '...': + found = True + break + line -= 1 + return line if found else start - 1 + + def _find_down_kw(self, start:int) -> int: + last_line = self.source_editor.GetLineCount() + if start > last_line - 3: + return start + text = self.source_editor.GetLine(start) + is_marker = self.first_non_space_content(text) # check if selection starts with marker + if is_marker == '...': + return start + is_marker = '...' + found = False + line = start + 2 + while is_marker == '...' and line < last_line - 2: + row = self.source_editor.GetLine(line) + is_marker = self.first_non_space_content(row) + if is_marker != '...': + found = True + break + line += 1 + return line if found else start + 1 def find_initial_keyword(self, start: int, up=True): if up: - if start <= 3: - return - text = self.source_editor.GetLine(start) - is_marker = self.first_non_space_content(text) # check if selection starts with marker - if is_marker == '...': - return start - is_marker = '...' - found = False - line = start - 2 - while is_marker == '...' and line > 2: - row = self.source_editor.GetLine(line) - is_marker = self.first_non_space_content(row) - if is_marker != '...': - found = True - break - line -= 1 - return line if found else start - 1 - else: - last_line = self.source_editor.GetLineCount() - if start > last_line - 3: - return - text = self.source_editor.GetLine(start) - is_marker = self.first_non_space_content(text) # check if selection starts with marker - if is_marker == '...': - return start - is_marker = '...' - found = False - line = start + 2 - while is_marker == '...' and line < last_line - 2: - row = self.source_editor.GetLine(line) - is_marker = self.first_non_space_content(row) - if is_marker != '...': - found = True - break - line += 1 - return line if found else start + 1 + return self._find_up_kw(start) + return self._find_down_kw(start) def first_non_space_content(self, text): start = self.first_non_space(text) @@ -1900,24 +1919,8 @@ def move_row_down(self, event): top_content = self.source_editor.GetLine(ini_line) # exception if is long assignement or arguments with continuation markers get below line is_marker = self.first_non_space_content(top_content) - # get the next row content and length - rowbelow = self.source_editor.GetLine(end_line + 1) if is_marker != '...': - if end_line + 2 < last_line - 1: - # exception if is long assignement or arguments with continuation markers get below line - is_marker = self.first_non_space_content(rowbelow) - if is_marker == '...': - new_end_line = self.find_initial_keyword(end_line, up=False) - if new_end_line != end_line: # Extend selection - new_end_pos = self.source_editor.GetLineEndPosition(new_end_line-1) - self.source_editor.SetSelection(start, new_end_pos - 1) - self.source_editor.SetAnchor(start) - # exception if target is long assignemnt or arguments with continuation markers get end line - # get the after below row content - rowafterbelow = self.source_editor.GetLine(new_end_line + 2) # Checking if is continuation arguments - is_marker = self.first_non_space_content(rowafterbelow) - if is_marker == '...': - new_ini_line = self.find_initial_keyword(new_end_line + 1, up=False) + new_ini_line = self._set_pos_by_marker(new_ini_line, new_end_line, end_line, last_line, start) self.source_editor.BeginUndoAction() if new_ini_line != ini_line: # Move block to down new_ini_line @@ -1954,10 +1957,35 @@ def move_row_down(self, event): if new_start > old_start and was_end_new and was_end_old: self.indent_line(new_ini_line - 1) # New selection + self._set_new_selection(new_ini_line, new_end_line) + + def _set_pos_by_marker(self, new_ini_line:int, new_end_line:int, end_line:int, last_line:int, start:int) -> int: + # get the next row content and length + rowbelow = self.source_editor.GetLine(end_line + 1) + if end_line + 2 < last_line - 1: + # exception if is long assignement or arguments with continuation markers get below line + is_marker = self.first_non_space_content(rowbelow) + if is_marker == '...': + new_end_line = self.find_initial_keyword(end_line, up=False) + if new_end_line != end_line: # Extend selection + new_end_pos = self.source_editor.GetLineEndPosition(new_end_line - 1) + self._select_anchor(start, new_end_pos - 1) + # exception if target is long assignemnt or arguments with continuation markers get end line + # get the after below row content + rowafterbelow = self.source_editor.GetLine(new_end_line + 2) # Checking if is continuation arguments + is_marker = self.first_non_space_content(rowafterbelow) + if is_marker == '...': + new_ini_line = self.find_initial_keyword(new_end_line + 1, up=False) + return new_ini_line + + def _set_new_selection(self, new_ini_line: int, new_end_line: int) -> None: nstartpos = self.source_editor.PositionFromLine(new_ini_line) nendpos = self.source_editor.GetLineEndPosition(new_end_line) - self.source_editor.SetSelection(nstartpos, nendpos) - self.source_editor.SetAnchor(nstartpos) + self._select_anchor(nstartpos, nendpos) + + def _select_anchor(self, start:int, end:int) -> None: + self.source_editor.SetSelection(start, end) + self.source_editor.SetAnchor(start) def delete_row(self, event): __ = event @@ -2430,29 +2458,25 @@ def on_key_pressed(self, event): # Tips if event.ShiftDown(): self.show_kw_doc() - ''' - self.CallTipSetBackground("yellow") - self.CallTipShow(pos, f"lots of of text: blah, blah, blah\n\n" - "show some suff, maybe parameters..\n\n" - f"fubar(param1, param2)\n\nContext: {selected}" - ) - ''' # Code completion else: self.parent.on_content_assist(event) self.key_trigger = 0 else: - if self.autocomplete and not event.ControlDown(): - if 32 < key < 256 and self.key_trigger > -1: - if self.key_trigger < 2: - self.key_trigger += 1 - else: - self.key_trigger = -1 - self.parent.on_content_assist(event) - else: - self.key_trigger = 0 + self._try_autocomplete(key, event) event.Skip() + def _try_autocomplete(self, key: int, event: wx.KeyEvent) -> None: + if self.autocomplete and not event.ControlDown(): + if 32 < key < 256 and self.key_trigger > -1: + if self.key_trigger < 2: + self.key_trigger += 1 + else: + self.key_trigger = -1 + self.parent.on_content_assist(event) + else: + self.key_trigger = 0 + def set_text(self, text): self.SetReadOnly(False) self.SetText(text) diff --git a/src/robotide/lib/compat/parsing/languages.py b/src/robotide/lib/compat/parsing/languages.py index 40e08bdc7..2d686d196 100644 --- a/src/robotide/lib/compat/parsing/languages.py +++ b/src/robotide/lib/compat/parsing/languages.py @@ -39,8 +39,7 @@ class Languages: print(lang.name, lang.code) """ - def __init__(self, languages: 'Iterable[LanguageLike]|LanguageLike|None' = (), - add_english: bool = True): + def __init__(self, languages: 'Iterable[LanguageLike]|LanguageLike|None' = (), add_english: bool = True): """ :param languages: Initial language or list of languages. Languages can be given as language codes or names, paths or names of @@ -98,7 +97,8 @@ def add_language(self, lang: LanguageLike): self._add_language(lang) self._bdd_prefix_regexp = None - def _exists(self, path: Path): + @staticmethod + def _exists(path: Path): try: return path.exists() except OSError: # Can happen on Windows w/ Python < 3.10. @@ -131,7 +131,8 @@ def _get_languages(self, languages, add_english=True) -> 'list[Language]': returned.extend(self._import_language_module(lang)) return returned - def _resolve_languages(self, languages, add_english=True): + @staticmethod + def _resolve_languages(languages, add_english=True): if not languages: languages = [] elif is_list_like(languages): @@ -142,7 +143,8 @@ def _resolve_languages(self, languages, add_english=True): languages.append(En()) return languages - def _get_available_languages(self) -> 'dict[str, type[Language]]': + @staticmethod + def _get_available_languages() -> 'dict[str, type[Language]]': available = {} for lang in Language.__subclasses__(): available[normalize(cast(str, lang.code), ignore='-')] = lang @@ -771,6 +773,9 @@ class Pl(Language): false_strings = ['Fałsz', 'Nie', 'Wyłączone', 'Nic'] +RU_RES = 'Ресурс' +RU_TES = 'Шаблон' + class Uk(Language): """Ukrainian""" settings_header = 'Налаштування' @@ -780,7 +785,7 @@ class Uk(Language): keywords_header = 'Ключових слова' comments_header = 'Коментарів' library_setting = 'Бібліотека' - resource_setting = 'Ресурс' + resource_setting = RU_RES variables_setting = 'Змінна' documentation_setting = 'Документація' metadata_setting = 'Метадані' @@ -800,7 +805,7 @@ class Uk(Language): tags_setting = 'Теги' setup_setting = 'Встановлення' teardown_setting = 'Cпростовувати пункт за пунктом' - template_setting = 'Шаблон' + template_setting = RU_TES timeout_setting = 'Час вийшов' arguments_setting = 'Аргументи' given_prefixes = ['Дано'] @@ -861,7 +866,7 @@ class Ru(Language): keywords_header = 'Ключевые слова' comments_header = 'Комментарии' library_setting = 'Библиотека' - resource_setting = 'Ресурс' + resource_setting = RU_RES variables_setting = 'Переменные' documentation_setting = 'Документация' metadata_setting = 'Метаданные' @@ -881,7 +886,7 @@ class Ru(Language): tags_setting = 'Метки' setup_setting = 'Инициализация' teardown_setting = 'Завершение' - template_setting = 'Шаблон' + template_setting = RU_TES timeout_setting = 'Лимит' arguments_setting = 'Аргументы' given_prefixes = ['Дано'] @@ -1065,7 +1070,7 @@ class Bg(Language): keywords_header = 'Ключови думи' comments_header = 'Коментари' library_setting = 'Библиотека' - resource_setting = 'Ресурс' + resource_setting = RU_RES variables_setting = 'Променлива' documentation_setting = 'Документация' metadata_setting = 'Метаданни' @@ -1085,7 +1090,7 @@ class Bg(Language): tags_setting = 'Етикети' setup_setting = 'Първоначални настройки' teardown_setting = 'Приключване' - template_setting = 'Шаблон' + template_setting = RU_TES timeout_setting = 'Таймаут' arguments_setting = 'Аргументи' given_prefixes = ['В случай че'] diff --git a/src/robotide/preferences/__init__.py b/src/robotide/preferences/__init__.py index 41d1e23c3..c966db937 100644 --- a/src/robotide/preferences/__init__.py +++ b/src/robotide/preferences/__init__.py @@ -19,7 +19,6 @@ from .imports import ImportPreferences from .saving import SavingPreferences from .settings import Settings, initialize_settings, RideSettings -from ..ui import ExcludePreferences import wx @@ -63,6 +62,7 @@ def remove(self, panel_class): self._preference_panels.remove(panel_class) def _add_builtin_preferences(self): + from ..ui import ExcludePreferences self.add(DefaultPreferences) self.add(SavingPreferences) self.add(ImportPreferences) diff --git a/src/robotide/preferences/editors.py b/src/robotide/preferences/editors.py index cfe87851b..d156d35e7 100644 --- a/src/robotide/preferences/editors.py +++ b/src/robotide/preferences/editors.py @@ -20,8 +20,12 @@ from wx.lib.masked import NumCtrl from .settings import RideSettings -from ..ui.preferences_dialogs import (PreferencesPanel, SpinChoiceEditor, IntegerChoiceEditor, boolean_editor, - StringChoiceEditor, PreferencesColorPicker) +# from robotide.ui.preferences_dialogs import PreferencesPanel +from ..ui import preferences_dialogs as pdiag +# from . import (PreferencesPanel, SpinChoiceEditor, IntegerChoiceEditor, boolean_editor, +# StringChoiceEditor, PreferencesColorPicker) +from robotide.ui.preferences_dialogs import PreferencesPanel + from ..widgets import Label from .managesettingsdialog import SaveLoadSettings from ..context import IS_WINDOWS @@ -126,7 +130,7 @@ def _get_path(): return join(dirname(abspath(__file__)), 'settings.cfg') def _create_font_editor(self): - f = IntegerChoiceEditor( + f = pdiag.IntegerChoiceEditor( self._settings, 'font size', _('Font Size'), [str(i) for i in range(8, 16)]) sizer = wx.FlexGridSizer(rows=4, cols=2, vgap=10, hgap=30) @@ -135,18 +139,18 @@ def _create_font_editor(self): sizer.AddMany([l_size, f.chooser(self)]) fixed_font = False if 'zoom factor' in self._settings: - z = SpinChoiceEditor( + z = pdiag.SpinChoiceEditor( self._settings, 'zoom factor', _('Zoom Factor'), (-10, 20)) l_zoom = z.label(self) set_colors(l_zoom, self.background_color, self.foreground_color) sizer.AddMany([l_zoom, z.chooser(self)]) if FIXED_FONT in self._settings: - l_ff, editor = boolean_editor(self, self._settings, FIXED_FONT, _('Use fixed width font')) + l_ff, editor = pdiag.boolean_editor(self, self._settings, FIXED_FONT, _('Use fixed width font')) set_colors(l_ff, self.background_color, self.foreground_color) sizer.AddMany([l_ff, editor]) fixed_font = self._settings[FIXED_FONT] if 'font face' in self._settings: - s = StringChoiceEditor(self._settings, 'font face', _('Font Face'), read_fonts(fixed_font)) + s = pdiag.StringChoiceEditor(self._settings, 'font face', _('Font Face'), read_fonts(fixed_font)) l_font = s.label(self) set_colors(l_font, self.background_color, self.foreground_color) sizer.AddMany([l_font, s.chooser(self)]) @@ -197,7 +201,7 @@ def create_colors_sizer(self): row += 1 label = wx.StaticText(self, wx.ID_ANY, label_text) set_colors(label, self.background_color, self.foreground_color) - button = PreferencesColorPicker( + button = pdiag.PreferencesColorPicker( self, wx.ID_ANY, self._settings, settings_key) container.Add(button, (row, column), flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=4) @@ -228,7 +232,7 @@ def on_reset(self, event): def _create_text_config_editor(self): settings = self._settings sizer = wx.FlexGridSizer(rows=2, cols=2, vgap=10, hgap=10) - l_auto_suggest, editor = boolean_editor(self, settings, 'enable auto suggestions', + l_auto_suggest, editor = pdiag.boolean_editor(self, settings, 'enable auto suggestions', _('Enable auto suggestions')) set_colors(l_auto_suggest, self.background_color, self.foreground_color) sizer.AddMany([l_auto_suggest, editor]) @@ -253,17 +257,17 @@ def _create_grid_config_editor(self): set_colors(l_col_size, self.background_color, self.foreground_color) sizer.Add(l_col_size) sizer.Add(self._number_editor(settings, 'col size')) - l_auto_size, editor = boolean_editor(self, settings, 'auto size cols', _('Auto size columns')) + l_auto_size, editor = pdiag.boolean_editor(self, settings, 'auto size cols', _('Auto size columns')) set_colors(l_auto_size, self.background_color, self.foreground_color) sizer.AddMany([l_auto_size, editor]) l_max_size = self._label_for(_('Max column size\n(applies when auto size is on)')) set_colors(l_max_size, self.background_color, self.foreground_color) sizer.Add(l_max_size) sizer.Add(self._number_editor(settings, 'max col size')) - l_word_wrap, editor = boolean_editor(self, settings, 'word wrap', _('Word wrap and auto size rows')) + l_word_wrap, editor = pdiag.boolean_editor(self, settings, 'word wrap', _('Word wrap and auto size rows')) set_colors(l_word_wrap, self.background_color, self.foreground_color) sizer.AddMany([l_word_wrap, editor]) - l_auto_suggest, editor = boolean_editor(self, settings, 'enable auto suggestions', + l_auto_suggest, editor = pdiag.boolean_editor(self, settings, 'enable auto suggestions', _('Enable auto suggestions')) set_colors(l_auto_suggest, self.background_color, self.foreground_color) sizer.AddMany([l_auto_suggest, editor]) @@ -306,7 +310,7 @@ def _create_foreground_pickers(self, colors_sizer): ): lbl = wx.StaticText(self, wx.ID_ANY, label) set_colors(lbl, self.background_color, self.foreground_color) - btn = PreferencesColorPicker( + btn = pdiag.PreferencesColorPicker( self, wx.ID_ANY, self._settings, key) self._color_pickers.append(btn) colors_sizer.Add(btn, (row, 2), @@ -329,7 +333,7 @@ def _create_background_pickers(self, colors_sizer): ): lbl = wx.StaticText(self, wx.ID_ANY, label) set_colors(lbl, self.background_color, self.foreground_color) - btn = PreferencesColorPicker( + btn = pdiag.PreferencesColorPicker( self, wx.ID_ANY, self._settings, key) self._color_pickers.append(btn) colors_sizer.Add(btn, (row, 0), @@ -373,9 +377,9 @@ def _create_test_runner_config_editor(self): add_colors = "-C ansi" else: add_colors = "-C on" - l_usecolor, usecolor = boolean_editor(self, settings, 'use colors', + l_usecolor, usecolor = pdiag.boolean_editor(self, settings, 'use colors', f"{_('Shows console colors set by')} {add_colors} ") - l_confirm, editor = boolean_editor(self, settings, 'confirm run', + l_confirm, editor = pdiag.boolean_editor(self, settings, 'confirm run', _('Asks for confirmation to run all tests if none selected ')) set_colors(l_confirm, self.background_color, self.foreground_color) set_colors(l_usecolor, self.background_color, self.foreground_color) @@ -400,7 +404,7 @@ def create_colors_sizer(self): row += 1 label = wx.StaticText(self, wx.ID_ANY, label_text) set_colors(label, self.background_color, self.foreground_color) - button = PreferencesColorPicker( + button = pdiag.PreferencesColorPicker( self, wx.ID_ANY, self._settings, settings_key) container.Add(button, (row, column), flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=4) diff --git a/src/robotide/ui/__init__.py b/src/robotide/ui/__init__.py index b225bb1d4..664a4662e 100644 --- a/src/robotide/ui/__init__.py +++ b/src/robotide/ui/__init__.py @@ -17,3 +17,5 @@ from .progress import LoadProgressObserver from .treeplugin import Tree from .mainframe import ToolBar +from .preferences_dialogs import (PreferencesPanel, SpinChoiceEditor, IntegerChoiceEditor, boolean_editor, + StringChoiceEditor, PreferencesColorPicker) diff --git a/src/robotide/ui/preferences_dialogs.py b/src/robotide/ui/preferences_dialogs.py index 62bbadf26..dc3208d88 100644 --- a/src/robotide/ui/preferences_dialogs.py +++ b/src/robotide/ui/preferences_dialogs.py @@ -17,9 +17,8 @@ import textwrap import wx -from wx import Colour -from ..preferences.settings import RideSettings +# from ..preferences.settings import RideSettings # DEBUG Removed to fix "cicular import" from ..context import IS_LINUX from ..widgets import HelpLabel, Label, TextField @@ -35,6 +34,7 @@ def __init__(self, parent=None, name_tr=None, *args, **kwargs): self.name_tr = name_tr # self.location = (_("Preferences"),) # self.title = _("Preferences") + from ..preferences.settings import RideSettings wx.Panel.__init__(self, parent, *args, **kwargs) self._gsettings = RideSettings() self.settings = self._gsettings['General'] @@ -71,6 +71,7 @@ def __init__(self, parent, id, settings, key, choices): self.key = key # wx.ComboBox(self, parent, id, self._get_value(), size=self._get_size(choices), # choices=choices, style=wx.CB_READONLY) + from ..preferences.settings import RideSettings super(PreferencesComboBox, self).__init__(parent, id, self._get_value(), size=self._get_size(choices), choices=choices, style=wx.CB_READONLY) @@ -119,6 +120,7 @@ class PreferencesSpinControl(wx.SpinCtrl): def __init__(self, parent, id, settings, key, choices): self.settings = settings self.key = key + from ..preferences.settings import RideSettings super(PreferencesSpinControl, self).__init__(parent, id, size=self._get_size(choices[-1])) @@ -160,7 +162,8 @@ def __init__(self, parent, id, settings, key): self.settings = settings self.key = key # print(f"DEBUG: Preferences ColourPicker value type {type(settings[key])}") - value = Colour(settings[key]) + value = wx.Colour(settings[key]) + from ..preferences.settings import RideSettings super(PreferencesColorPicker, self).__init__(parent, id, colour=value) self._gsettings = RideSettings() self.psettings = self._gsettings['General'] @@ -192,6 +195,7 @@ def __init__(self, settings, setting_name, label, choices, help=''): self._label = label self._choices = choices self._help = help + from ..preferences.settings import RideSettings self._gsettings = RideSettings() self.csettings = self._gsettings['General'] self.background_color = self.csettings['background'] @@ -231,6 +235,7 @@ class SpinChoiceEditor(_ChoiceEditor): def boolean_editor(parent, settings, name, label, help=''): editor = _create_checkbox_editor(parent, settings, name, help) + from ..preferences.settings import RideSettings _gsettings = RideSettings() bsettings = _gsettings['General'] background_color = bsettings['background'] @@ -255,6 +260,7 @@ def _create_checkbox_editor(parent, settings, name, help): def comma_separated_value_editor(parent, settings, name, label, thelp=''): initial_value = ', '.join(settings.get(name, "")) editor = TextField(parent, initial_value) + from ..preferences.settings import RideSettings _gsettings = RideSettings() esettings = _gsettings['General'] background_color = esettings['background'] diff --git a/src/robotide/version.py b/src/robotide/version.py index 00e2871c5..e6fdc8619 100644 --- a/src/robotide/version.py +++ b/src/robotide/version.py @@ -15,4 +15,4 @@ # # Automatically generated by `tasks.py`. -VERSION = 'v2.1' +VERSION = 'v2.1.1' diff --git a/utest/editor/test_texteditor.py b/utest/editor/test_texteditor.py index 5806e7e7b..82f81726f 100644 --- a/utest/editor/test_texteditor.py +++ b/utest/editor/test_texteditor.py @@ -163,14 +163,14 @@ def setUp(self): self.plugin._open_tree_selection_in_editor() self.app.frame.SetStatusText("File:" + self.app.project.data.source) # Uncomment next line (and MainLoop in tests) if you want to see the app - self.frame.Show() + # self.frame.Show() def tearDown(self): self.plugin.unsubscribe_all() PUBLISHER.unsubscribe_all() self.app.project.close() - wx.CallAfter(self.app.ExitMainLoop) - self.app.MainLoop() # With this here, there is no Segmentation fault + # wx.CallAfter(self.app.ExitMainLoop) + # self.app.MainLoop() # With this here, there is no Segmentation fault # wx.CallAfter(wx.Exit) self.shared_mem.shm.close() self.shared_mem.shm.unlink() @@ -476,8 +476,8 @@ def test_check_variables_section(self): print(f"DEBUG: after_apply len={len(after_apply)} initial content len={len(content)}:\n{after_apply}") # assert fulltext == content[0] + content[1] + content[2] # Uncomment next lines if you want to see the app - wx.CallLater(5000, self.app.ExitMainLoop) - self.app.MainLoop() + # wx.CallLater(5000, self.app.ExitMainLoop) + # self.app.MainLoop() @pytest.mark.skipif(os.sep == '\\', reason="Causes exception on Windows") def test_get_selected_or_near_text(self): @@ -593,9 +593,34 @@ def test_get_selected_or_near_text(self): assert position == text_length # Uncomment next lines if you want to see the app - wx.CallLater(5000, self.app.ExitMainLoop) - self.app.MainLoop() + # wx.CallLater(5000, self.app.ExitMainLoop) + # self.app.MainLoop() + + @pytest.mark.skipif(os.sep == '\\', reason="Causes exception on Windows") + def test_miscellanous(self): + spaces = ' ' * self.plugin._editor_component.tab_size + content = [spaces + 'Log' + spaces + 'This is ${unknown}\n'] + pos = len(spaces + 'Log') + self.plugin._editor_component.source_editor.set_text(''.join(content)) + self.plugin._editor_component.source_editor.SetAnchor(pos) + self.plugin._editor_component.source_editor.SetSelection(pos - len('Log'), pos) + stylizer = self.plugin._editor_component.source_editor.stylizer + fulltext = self.plugin._editor_component.source_editor.GetText() + font_size = stylizer._font_size() + font_face = stylizer._font_face() + zoom_factor = stylizer._zoom_factor() + assert font_size == 10 + assert font_face in ["Noto Sans", "Courier New"] + assert zoom_factor == 0 + # print(f"DEBUG: fulltext:\n{fulltext}") + stylizer.set_styles(True) + stylizer.stylize() + + assert fulltext == spaces + 'Log' + spaces + 'This is ${unknown}\n' + # Uncomment next lines if you want to see the app + # wx.CallLater(5000, self.app.ExitMainLoop) + # self.app.MainLoop() class TestLanguageFunctions(unittest.TestCase): @@ -623,14 +648,14 @@ def setUp(self): self.plugin._open_tree_selection_in_editor() self.app.frame.SetStatusText("File:" + self.app.project.data.source) # Uncomment next line (and MainLoop in tests) if you want to see the app - self.frame.Show() + # self.frame.Show() def tearDown(self): self.plugin.unsubscribe_all() PUBLISHER.unsubscribe_all() self.app.project.close() - wx.CallAfter(self.app.ExitMainLoop) - self.app.MainLoop() # With this here, there is no Segmentation fault + # wx.CallAfter(self.app.ExitMainLoop) + # self.app.MainLoop() # With this here, there is no Segmentation fault # wx.CallAfter(wx.Exit) self.shared_mem.shm.close() self.shared_mem.shm.unlink() @@ -679,8 +704,8 @@ def test_read_language(self): # print(f"DEBUG: fulltext:\n{fulltext}") # assert fulltext == spaces + '1 - Line one' + spaces + 'with cells' + spaces + spaces + 'last text\n' # Uncomment next lines if you want to see the app - wx.CallLater(5000, self.app.ExitMainLoop) - self.app.MainLoop() + # wx.CallLater(5000, self.app.ExitMainLoop) + # self.app.MainLoop() def test_get_rf_lang_code(self): lang = ['en'] @@ -709,7 +734,7 @@ def test_obtain_language(self): content = fp.readlines() content = "".join(content) self.plugin._editor_component.source_editor.set_text(content) - # print(f"DEBUG: content:\n{content}") + print(f"DEBUG: content:\n{content}") for lang in LANGUAGES: self.set_language(lang[0]) diff --git a/utest/resources/fake.cfg b/utest/resources/fake.cfg index 0d9bed0a6..cedbfd110 100644 --- a/utest/resources/fake.cfg +++ b/utest/resources/fake.cfg @@ -3,8 +3,29 @@ auto imports = [] pythonpath = [] global_settings = [] doc language = '' -excludes = None -txt number of spaces = 2 +library xml directories = [] +reformat = False +txt number of spaces = 4 [General] font size = 10 [Plugins] +[[Text Editor]] +[Text Edit] +setting = 'black' +background = 'white' +caret style = 'block' +enable auto suggestions = False +font face = 'Noto Sans' +argument = '#bb8844' +comment = 'black' +error = 'black' +gherkin = 'black' +heading = '#999999' +import = '#555555' +keyword = '#990000' +separator = 'black' +syntax = 'black' +tc_kw_name = '#aaaaaa' +variable = '#008080' +font size = 10 +zoom factor = 0 From 2a28c6b971fe4ff0e0907fd2e71135ee698643cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9lio=20Guilherme?= Date: Sat, 26 Oct 2024 03:30:01 +0100 Subject: [PATCH 02/23] Cleanup code (#154) * Refactor TextEdit, WIP * Fix unit tests with language transform * Separated key commands in TextEdit. Breaks some, TAB functions, Enter.. * Delete commented code, renamve arguments * Refactor change_locale * Increase unit tests --- src/robotide/application/application.py | 95 ++++-------------------- src/robotide/editor/kweditor.py | 3 - src/robotide/preferences/editors.py | 5 -- src/robotide/ui/preferences_dialogs.py | 35 +++++---- utest/application/test_app_main.py | 14 ++++ utest/contrib/testrunner/test_process.py | 21 +++++- 6 files changed, 67 insertions(+), 106 deletions(-) diff --git a/src/robotide/application/application.py b/src/robotide/application/application.py index 835a1ab2c..1cf9df50e 100644 --- a/src/robotide/application/application.py +++ b/src/robotide/application/application.py @@ -26,7 +26,6 @@ from ..ui.mainframe import RideFrame from .. import publish from .. import context, contrib -# from ..context import coreplugins from ..preferences import Preferences, RideSettings from ..application.pluginloader import PluginLoader from ..application.editorprovider import EditorProvider @@ -176,27 +175,14 @@ def _ApplyThemeToWidget(widget, fore_color=wx.BLUE, back_color=wx.LIGHT_GREY, th aui_default_tool_bar_art = AuiDefaultToolBarArt() aui_default_tool_bar_art.SetDefaultColours(wx.GREEN) widget.SetBackgroundColour(background) - # widget.SetOwnBackgroundColour(background) widget.SetForegroundColour(foreground) - # widget.SetOwnForegroundColour(foreground) - """ - widget.SetBackgroundColour(Colour(200, 222, 40)) - widget.SetOwnBackgroundColour(Colour(200, 222, 40)) - widget.SetForegroundColour(Colour(7, 0, 70)) - widget.SetOwnForegroundColour(Colour(7, 0, 70)) - """ - # or elif isinstance(widget, wx.Control): if not isinstance(widget, (wx.Button, wx.BitmapButton, ButtonWithHandler)): widget.SetForegroundColour(foreground) widget.SetBackgroundColour(background) - # widget.SetOwnBackgroundColour(background) - # widget.SetOwnForegroundColour(foreground) else: widget.SetForegroundColour(secondary_foreground) widget.SetBackgroundColour(secondary_background) - # widget.SetOwnBackgroundColour(secondary_background) - # widget.SetOwnForegroundColour(secondary_foreground) elif isinstance(widget, (wx.TextCtrl, TabFrame, AuiTabCtrl)): widget.SetForegroundColour(foreground_text) # or fore_color widget.SetBackgroundColour(background_help) # or back_color @@ -206,19 +192,14 @@ def _ApplyThemeToWidget(widget, fore_color=wx.BLUE, back_color=wx.LIGHT_GREY, th elif isinstance(widget, wx.MenuItem): widget.SetTextColour(foreground) widget.SetBackgroundColour(background) - # print(f"DEBUG: Application ApplyTheme wx.MenuItem {type(widget)}") else: widget.SetBackgroundColour(background) - # widget.SetOwnBackgroundColour(background) widget.SetForegroundColour(foreground) - # widget.SetOwnForegroundColour(foreground) def _WalkWidgets(self, widget, indent=0, indent_level=4, theme=None): - # print(' ' * indent + widget.__class__.__name__) if theme is None: theme = {} widget.Freeze() - # print(f"DEBUG Application General : _WalkWidgets background {theme['background']}") self._ApplyThemeToWidget(widget=widget, theme=theme) for child in widget.GetChildren(): if not child.IsTopLevel(): # or isinstance(child, wx.PopupWindow)): @@ -230,7 +211,6 @@ def _WalkWidgets(self, widget, indent=0, indent_level=4, theme=None): def SetGlobalColour(self, message): if message.keys[0] != "General": return - # print(f"DEBUG Application General : Enter SetGlobalColour message= {message.keys[0]}") app = wx.App.Get() _root = app.GetTopWindow() theme = self.settings.get_without_default('General') @@ -241,7 +221,6 @@ def SetGlobalColour(self, message): font.SetPointSize(font_size) _root.SetFont(font) self._WalkWidgets(_root, theme=theme) - # print(f"DEBUG Application General : SetGlobalColour AppliedWidgets check Filexplorer and Tree") if theme['apply to panels'] and self.fileexplorerplugin.settings['_enabled']: self.fileexplorerplugin.settings['background'] = theme['background'] self.fileexplorerplugin.settings['foreground'] = theme['foreground'] @@ -260,69 +239,12 @@ def SetGlobalColour(self, message): self.treeplugin.settings[FONT_FACE] = theme[FONT_FACE] if self.treeplugin.settings['opened']: self.treeplugin.on_show_tree(None) - """ - all_windows = list() - general = self.settings.get('General', None) - # print(f"DEBUG: Application General {general['background']} Type message {type(message)}") - # print(f"DEBUG: Application General message keys {message.keys} old {message.old} new {message.new}") - background = general['background'] - foreground = general['foreground'] - background_help = general[BACKGROUND_HELP] - foreground_text = general[FOREGROUND_TEXT] - font_size = general[FONT_SIZE] - font_face = general[FONT_FACE] - font = _root.GetFont() - font.SetFaceName(font_face) - font.SetPointSize(font_size) - _root.SetFont(font) - - def _iterate_all_windows(root): - if hasattr(root, 'GetChildren'): - children = root.GetChildren() - if children: - for c in children: - _iterate_all_windows(c) - all_windows.append(root) - - _iterate_all_windows(_root) - - for w in all_windows: - if hasattr(w, 'SetHTMLBackgroundColour'): - w.SetHTMLBackgroundColour(wx.Colour(background_help)) - w.SetForegroundColour(wx.Colour(foreground_text)) # 7, 0, 70)) - elif hasattr(w, 'SetBackgroundColour'): - w.SetBackgroundColour(wx.Colour(background)) # 44, 134, 179)) - - # if hasattr(w, 'SetOwnBackgroundColour'): - # w.SetOwnBackgroundColour(wx.Colour(background)) # 44, 134, 179)) - - if hasattr(w, 'SetForegroundColour'): - w.SetForegroundColour(wx.Colour(foreground)) # 7, 0, 70)) - - # if hasattr(w, 'SetOwnForegroundColour'): - # w.SetOwnForegroundColour(wx.Colour(foreground)) # 7, 0, 70)) - - if hasattr(w, 'SetFont'): - w.SetFont(font) - """ def change_locale(self, message): if message.keys[0] != "General": return initial_locale = self._locale.GetName() - if languages: - from ..preferences import Languages - names = [n for n in Languages.names] - else: - names = [('English', 'en', wx.LANGUAGE_ENGLISH)] - general = self.settings.get_without_default('General') - language = general.get('ui language', 'English') - try: - idx = [lang[0] for lang in names].index(language) - code = names[idx][2] - except (IndexError, ValueError): - print(f"DEBUG: application.py RIDE change_locale ERROR: Could not find {language=}") - code = wx.LANGUAGE_ENGLISH_WORLD + code = self._get_language_code() del self._locale self._locale = wx.Locale(code) if not self._locale.IsOk(): @@ -352,6 +274,21 @@ def change_locale(self, message): except FileNotFoundError: pass + def _get_language_code(self) -> str: + if languages: + from ..preferences import Languages + names = [n for n in Languages.names] + else: + names = [('English', 'en', wx.LANGUAGE_ENGLISH)] + general = self.settings.get_without_default('General') + language = general.get('ui language', 'English') + try: + idx = [lang[0] for lang in names].index(language) + code = names[idx][2] + except (IndexError, ValueError): + print(f"DEBUG: application.py RIDE change_locale ERROR: Could not find {language=}") + code = wx.LANGUAGE_ENGLISH_WORLD + return code @staticmethod def update_excludes(message): diff --git a/src/robotide/editor/kweditor.py b/src/robotide/editor/kweditor.py index dcf66db8e..9826959a3 100755 --- a/src/robotide/editor/kweditor.py +++ b/src/robotide/editor/kweditor.py @@ -845,7 +845,6 @@ def _call_alt_function(self, event, keycode: int): return True def on_key_down(self, event): - # print(f"DEBUG: KeywordEditor on_key_down event={event} focus={self.is_focused()}") keycode = event.GetUnicodeKey() or event.GetKeyCode() if event.ControlDown(): if event.ShiftDown(): @@ -860,7 +859,6 @@ def on_key_down(self, event): event.Skip() def on_char(self, event): - # print(f"DEBUG: KeywordEditor on_char event={event} focus={self.is_focused()}") key_char = event.GetUnicodeKey() if key_char < ord(' '): return @@ -950,7 +948,6 @@ def move_grid_cursor_and_edit(self): self.open_cell_editor() def on_key_up(self, event): - # print(f"DEBUG: KeywordEditor on_key_up event={event} focus={self.is_focused()}") event.Skip() # DEBUG seen this skip as soon as possible self._tooltips.hide() self._hide_link_if_necessary() diff --git a/src/robotide/preferences/editors.py b/src/robotide/preferences/editors.py index d156d35e7..e25e44deb 100644 --- a/src/robotide/preferences/editors.py +++ b/src/robotide/preferences/editors.py @@ -16,19 +16,14 @@ from os.path import abspath, dirname, join import builtins import wx -from wx import Colour from wx.lib.masked import NumCtrl from .settings import RideSettings -# from robotide.ui.preferences_dialogs import PreferencesPanel from ..ui import preferences_dialogs as pdiag -# from . import (PreferencesPanel, SpinChoiceEditor, IntegerChoiceEditor, boolean_editor, -# StringChoiceEditor, PreferencesColorPicker) from robotide.ui.preferences_dialogs import PreferencesPanel from ..widgets import Label from .managesettingsdialog import SaveLoadSettings -from ..context import IS_WINDOWS from functools import lru_cache try: # import installed version first diff --git a/src/robotide/ui/preferences_dialogs.py b/src/robotide/ui/preferences_dialogs.py index dc3208d88..881bba2d7 100644 --- a/src/robotide/ui/preferences_dialogs.py +++ b/src/robotide/ui/preferences_dialogs.py @@ -18,7 +18,6 @@ import wx -# from ..preferences.settings import RideSettings # DEBUG Removed to fix "cicular import" from ..context import IS_LINUX from ..widgets import HelpLabel, Label, TextField @@ -66,13 +65,11 @@ def Separator(self, parent, title): class PreferencesComboBox(wx.ComboBox): """A combobox tied to a specific setting. Saves value to disk after edit.""" - def __init__(self, parent, id, settings, key, choices): + def __init__(self, parent, elid, settings, key, choices): self.settings = settings self.key = key - # wx.ComboBox(self, parent, id, self._get_value(), size=self._get_size(choices), - # choices=choices, style=wx.CB_READONLY) from ..preferences.settings import RideSettings - super(PreferencesComboBox, self).__init__(parent, id, self._get_value(), + super(PreferencesComboBox, self).__init__(parent, elid, self._get_value(), size=self._get_size(choices), choices=choices, style=wx.CB_READONLY) self._gsettings = RideSettings() @@ -86,13 +83,14 @@ def __init__(self, parent, id, settings, key, choices): def _get_value(self): return self.settings[self.key] - def _get_size(self, choices=[]): + @staticmethod + def _get_size(choices=None): """ In Linux with GTK3 wxPython 4, there was not enough spacing. The value 72 is there for 2 digits numeric lists, for IntegerPreferenceComboBox. This issue only occurs in Linux, for Mac and Windows using default size. """ - if IS_LINUX and choices: + if IS_LINUX and isinstance(choices, list): return wx.Size(max(max(len(str(s)) for s in choices) * 9, 144), 30) return wx.DefaultSize @@ -117,11 +115,11 @@ def _set_value(self, value): class PreferencesSpinControl(wx.SpinCtrl): """A spin control tied to a specific setting. Saves value to disk after edit.""" - def __init__(self, parent, id, settings, key, choices): + def __init__(self, parent, elid, settings, key, choices): self.settings = settings self.key = key from ..preferences.settings import RideSettings - super(PreferencesSpinControl, self).__init__(parent, id, + super(PreferencesSpinControl, self).__init__(parent, elid, size=self._get_size(choices[-1])) self._gsettings = RideSettings() @@ -138,7 +136,8 @@ def __init__(self, parent, id, settings, key, choices): def _get_value(self): return self.settings[self.key] - def _get_size(self, max_value): + @staticmethod + def _get_size(max_value): """ In Linux with GTK3 wxPython 4, there was not enough spacing. The value 72 is there for 2 digits numeric lists, for IntegerPreferenceComboBox. @@ -158,13 +157,13 @@ def _set_value(self, value): class PreferencesColorPicker(wx.ColourPickerCtrl): """A colored button that opens a color picker dialog""" - def __init__(self, parent, id, settings, key): + def __init__(self, parent, elid, settings, key): self.settings = settings self.key = key # print(f"DEBUG: Preferences ColourPicker value type {type(settings[key])}") value = wx.Colour(settings[key]) from ..preferences.settings import RideSettings - super(PreferencesColorPicker, self).__init__(parent, id, colour=value) + super(PreferencesColorPicker, self).__init__(parent, elid, colour=value) self._gsettings = RideSettings() self.psettings = self._gsettings['General'] background_color = self.psettings['background'] @@ -189,12 +188,12 @@ def SetColour(self, colour): class _ChoiceEditor(object): _editor_class = None - def __init__(self, settings, setting_name, label, choices, help=''): + def __init__(self, settings, setting_name, label, choices, elhelp=''): self._settings = settings self._setting_name = setting_name self._label = label self._choices = choices - self._help = help + self._help = elhelp from ..preferences.settings import RideSettings self._gsettings = RideSettings() self.csettings = self._gsettings['General'] @@ -233,8 +232,8 @@ class SpinChoiceEditor(_ChoiceEditor): _editor_class = PreferencesSpinControl -def boolean_editor(parent, settings, name, label, help=''): - editor = _create_checkbox_editor(parent, settings, name, help) +def boolean_editor(parent, settings, name, label, elhelp=''): + editor = _create_checkbox_editor(parent, settings, name, elhelp) from ..preferences.settings import RideSettings _gsettings = RideSettings() bsettings = _gsettings['General'] @@ -248,12 +247,12 @@ def boolean_editor(parent, settings, name, label, help=''): return blabel, editor -def _create_checkbox_editor(parent, settings, name, help): +def _create_checkbox_editor(parent, settings, name, elhelp): initial_value = settings.get(name, "") editor = wx.CheckBox(parent) editor.SetValue(initial_value) editor.Bind(wx.EVT_CHECKBOX, lambda evt: settings.set(name, editor.GetValue())) - editor.SetToolTip(help) + editor.SetToolTip(elhelp) return editor diff --git a/utest/application/test_app_main.py b/utest/application/test_app_main.py index fcf5b9e2f..c4d9d951d 100644 --- a/utest/application/test_app_main.py +++ b/utest/application/test_app_main.py @@ -230,5 +230,19 @@ def test_replace_std_for_win(self): robotide._replace_std_for_win() +class TestMisc(unittest.TestCase): + + def tearDown(self): + builtins.__import__ = real_import + + def test_get_code(self): + import wx + from robotide.application import RIDE + + main_app = RIDE() + code = main_app._get_language_code() + assert code in (175, wx.LANGUAGE_ENGLISH_WORLD, wx.LANGUAGE_PORTUGUESE) + + if __name__ == '__main__': unittest.main() diff --git a/utest/contrib/testrunner/test_process.py b/utest/contrib/testrunner/test_process.py index ff0bdf3f4..616f8bd17 100644 --- a/utest/contrib/testrunner/test_process.py +++ b/utest/contrib/testrunner/test_process.py @@ -20,7 +20,25 @@ from robotide.contrib.testrunner.testrunner import Process -if VERSION >= '4.0': +if VERSION >= '7.1.1': + console_out = b"==============================================================================\n" \ + b"Small Test \n" \ + b"==============================================================================\n" \ + b"Small Test.Test \n" \ + b"==============================================================================\n" \ + b"Passing | PASS |\n" \ + b"------------------------------------------------------------------------------\n" \ + b"Failing | FAIL |\n" \ + b"this fails\n" \ + b"------------------------------------------------------------------------------\n" \ + b"Small Test.Test | FAIL |\n" \ + b"2 tests, 1 passed, 1 failed\n" \ + b"==============================================================================\n" \ + b"Small Test | FAIL |\n" \ + b"2 tests, 1 passed, 1 failed\n" \ + b"==============================================================================\n" \ + b"Output: NONE\n" +elif VERSION >= '4.0': console_out = b"==============================================================================\n" \ b"Small Test \n" \ b"==============================================================================\n" \ @@ -73,6 +91,7 @@ def test_running_robot_test(self): print(output, errors) parsed_output = bytes(output.replace(b'\r', b'')) parsed_errors = bytes(errors.replace(b'\r', b'')) + assert parsed_output == console_out self.assertTrue(parsed_output.startswith(console_out), msg=repr(output)) # Because of deprecation messages in RF 3.1, from Equal to Regex self.assertRegex(parsed_errors, b".*\\[ WARN \\] this passes\n") From 778ef347a4c3d2cf71e1457a480d64d1e4bb91ed Mon Sep 17 00:00:00 2001 From: HelioGuilherme66 Date: Sat, 26 Oct 2024 23:39:12 +0100 Subject: [PATCH 03/23] Improve unit test --- utest/application/test_app_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utest/application/test_app_main.py b/utest/application/test_app_main.py index c4d9d951d..09a31e2c4 100644 --- a/utest/application/test_app_main.py +++ b/utest/application/test_app_main.py @@ -241,7 +241,7 @@ def test_get_code(self): main_app = RIDE() code = main_app._get_language_code() - assert code in (175, wx.LANGUAGE_ENGLISH_WORLD, wx.LANGUAGE_PORTUGUESE) + assert code in (wx.LANGUAGE_ENGLISH, wx.LANGUAGE_ENGLISH_WORLD, wx.LANGUAGE_PORTUGUESE) if __name__ == '__main__': From e80acfcab446c67cb3ddfe98f532ce259f492d0b Mon Sep 17 00:00:00 2001 From: HelioGuilherme66 Date: Sun, 27 Oct 2024 00:19:43 +0100 Subject: [PATCH 04/23] Change update from develop branch --- src/robotide/application/updatenotifier.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/robotide/application/updatenotifier.py b/src/robotide/application/updatenotifier.py index 2cf3949ca..ec3ecfcd2 100644 --- a/src/robotide/application/updatenotifier.py +++ b/src/robotide/application/updatenotifier.py @@ -102,12 +102,12 @@ def _get_rf_pypi_data(self): def upgrade_from_dev_dialog(version_installed, notebook, show_no_update=False): dev_version = urllib2.urlopen('https://raw.githubusercontent.com/robotframework/' - 'RIDE/master/src/robotide/version.py', timeout=1).read().decode('utf-8') + 'RIDE/develop/src/robotide/version.py', timeout=1).read().decode('utf-8') matches = re.findall(r"VERSION\s*=\s*'([\w.]*)'", dev_version) version_latest = matches[0] if matches else None if cmp_versions(version_installed, version_latest) == -1: # Here is the Menu Help->Upgrade insertion part, try to highlight menu # wx.CANCEL_DEFAULT - command = sys.executable + " -m pip install -U https://github.com/robotframework/RIDE/archive/master.zip" + command = sys.executable + " -m pip install -U https://github.com/robotframework/RIDE/archive/develop.zip" _add_content_to_clipboard(command) if not _askyesno(_("Upgrade?"), f"{SPC}{_('New development version is available.')}{SPC}\n{SPC}" f"{_('You may install version %s with:') % version_latest}\n" From a458f47cb6266464712f71300d182aeaccda0741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9lio=20Guilherme?= Date: Sun, 27 Oct 2024 01:21:43 +0000 Subject: [PATCH 05/23] Cleanup code (#156) * Refactor TextEdit, WIP * Fix unit tests with language transform * Separated key commands in TextEdit. Breaks some, TAB functions, Enter.. * Delete commented code, renamve arguments * Refactor change_locale * Increase unit tests * Improve unit test * Improve unit test * Change update from develop branch * Remove comments --- src/robotide/editor/listeditor.py | 4 +--- src/robotide/preferences/editor.py | 17 ----------------- src/robotide/ui/preferences_dialogs.py | 4 ---- src/robotide/widgets/htmlwnd.py | 2 -- utest/application/test_app_main.py | 3 --- 5 files changed, 1 insertion(+), 29 deletions(-) diff --git a/src/robotide/editor/listeditor.py b/src/robotide/editor/listeditor.py index a383b671c..69f5130ef 100644 --- a/src/robotide/editor/listeditor.py +++ b/src/robotide/editor/listeditor.py @@ -36,6 +36,7 @@ class ListEditorBase(wx.Panel): _buttons_nt = [] def __init__(self, parent, columns, controller, label=None): + __ = label self._menu = [_('Edit'), _('Move Up\tCtrl-Up'), _('Move Down\tCtrl-Down'), '---', _('Delete')] wx.Panel.__init__(self, parent) from ..preferences import RideSettings @@ -195,10 +196,7 @@ def __init__(self, parent, columns, color_foreground='black', self.color_foreground = color_foreground self.color_background = color_background self.SetBackgroundColour(Colour(color_background)) - # self.SetOwnBackgroundColour(Colour(color_background)) self.SetForegroundColour(Colour(color_foreground)) - # self.SetOwnForegroundColour(Colour(color_foreground)) - # self.EnableAlternateRowColours(True) self._parent = parent self._doc_language = None self.set_language() diff --git a/src/robotide/preferences/editor.py b/src/robotide/preferences/editor.py index bfa1f0723..76aaf061c 100644 --- a/src/robotide/preferences/editor.py +++ b/src/robotide/preferences/editor.py @@ -64,11 +64,7 @@ def __init__(self, parent, title, preferences, style="auto"): self.font.SetPointSize(self._general_settings[FONT_SIZE]) self.SetFont(self.font) self.SetBackgroundColour(Colour(self._general_settings['background'])) - ## self.SetOwnBackgroundColour(Colour(200, 222, 40)) - ## self.SetOwnForegroundColour(Colour(7, 0, 70)) - # self.SetOwnBackgroundColour(Colour(self._general_settings['secondary background'])) self.SetForegroundColour(Colour(self._general_settings['foreground'])) - # self.SetOwnForegroundColour(Colour(self._general_settings['secondary foreground'])) self._closing = False panels = preferences.preference_panels @@ -224,10 +220,6 @@ def __init__(self, *args, **kwargs): self.title.SetFont(font) self.title.SetForegroundColour(self.settings['foreground']) self.title.SetBackgroundColour(self.settings['background']) - # self.title.SetBackgroundColour(Colour(200, 222, 40)) - # self.title.SetForegroundColour(Colour(7, 0, 70)) - # self.SetBackgroundColour(Colour(200, 222, 40)) - # self.SetForegroundColour(Colour(7, 0, 70)) self.SetForegroundColour(self.settings['foreground']) self.SetBackgroundColour(self.settings['background']) @@ -242,17 +234,8 @@ def ShowPanel(self, panel): if self._current_panel is not None: self._current_panel.Hide() self._current_panel = panel - ## self.SetForegroundColour(self.settings['foreground']) - ## self.SetOwnBackgroundColour(self.settings['background']) - ## self.SetOwnForegroundColour(Colour(255, 255, 255)) - ## self.SetBackgroundColour(self.settings['background']) panel.SetForegroundColour(self.settings['foreground']) # Critical text all black on panel.SetBackgroundColour(self.settings['background']) # Black background - ## self.SetBackgroundColour(Colour(200, 222, 40)) - ## self.SetForegroundColour(Colour(7, 0, 70)) - ## panel.SetBackgroundColour(Colour(200, 222, 40)) - ## panel.SetForegroundColour(Colour(255, 255, 255)) - ## panel.SetForegroundColour(Colour(7, 0, 70)) panel.Show() sizer = self.panels_container.GetSizer() item = sizer.GetItem(panel) diff --git a/src/robotide/ui/preferences_dialogs.py b/src/robotide/ui/preferences_dialogs.py index 881bba2d7..0b12c4cf5 100644 --- a/src/robotide/ui/preferences_dialogs.py +++ b/src/robotide/ui/preferences_dialogs.py @@ -31,8 +31,6 @@ class PreferencesPanel(wx.Panel): def __init__(self, parent=None, name_tr=None, *args, **kwargs): self.tree_item = None self.name_tr = name_tr - # self.location = (_("Preferences"),) - # self.title = _("Preferences") from ..preferences.settings import RideSettings wx.Panel.__init__(self, parent, *args, **kwargs) self._gsettings = RideSettings() @@ -275,8 +273,6 @@ def set_value(evt): evt.Skip() editor.Bind(wx.EVT_KILL_FOCUS, lambda evt: set_value(evt)) elabel = Label(parent, label=label) - # background_color = esettings['background'] - # foreground_color = esettings['foreground'] elabel.SetBackgroundColour(background_color) elabel.SetForegroundColour(foreground_color) return elabel, editor diff --git a/src/robotide/widgets/htmlwnd.py b/src/robotide/widgets/htmlwnd.py index 8147ebc3b..9eb1204d1 100644 --- a/src/robotide/widgets/htmlwnd.py +++ b/src/robotide/widgets/htmlwnd.py @@ -34,8 +34,6 @@ def __init__(self, parent, size=wx.DefaultSize, text=None): self.SetBorders(2) self.SetStandardFonts(size=9) self.SetBackgroundColour(Colour(200, 222, 40)) - # self.SetOwnBackgroundColour(Colour(200, 222, 40)) - # self.SetOwnForegroundColour(Colour(7, 0, 70)) if text: self.set_content(text) self.SetHTMLBackgroundColour(Colour(general_settings[BACKGROUND_HELP])) diff --git a/utest/application/test_app_main.py b/utest/application/test_app_main.py index 09a31e2c4..b8caccbca 100644 --- a/utest/application/test_app_main.py +++ b/utest/application/test_app_main.py @@ -232,9 +232,6 @@ def test_replace_std_for_win(self): class TestMisc(unittest.TestCase): - def tearDown(self): - builtins.__import__ = real_import - def test_get_code(self): import wx from robotide.application import RIDE From dc72b49c950ea19f93d5a807ee59ab7ea997fe81 Mon Sep 17 00:00:00 2001 From: HelioGuilherme66 Date: Sun, 27 Oct 2024 04:00:10 +0000 Subject: [PATCH 06/23] Add configobj as a submodule --- .github/workflows/linux.yml | 1 + .github/workflows/sonar.yml | 1 + .gitmodules | 3 + src/robotide/preferences/__init__.py | 1 + src/robotide/preferences/configobj | 1 + src/robotide/preferences/configobj.py | 2336 ----------------- .../preferences/managesettingsdialog.py | 5 +- src/robotide/preferences/settings.py | 9 +- tasks.py | 4 + 9 files changed, 19 insertions(+), 2342 deletions(-) create mode 100644 .gitmodules create mode 160000 src/robotide/preferences/configobj delete mode 100644 src/robotide/preferences/configobj.py diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 3d483dd8d..a0b98937b 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -79,6 +79,7 @@ jobs: sudo dnf install -y sdl12-compat python3-wxpython4 xorg-x11-server-Xvfb python3-pip psmisc sudo dnf downgrade -y mesa* --refresh sudo -H pip install -r requirements-dev.txt + git submodule update - name: Run tests run: | Xvfb & diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index abdd02af6..ce0f8e79a 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -31,6 +31,7 @@ jobs: run: pip install https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-22.04/wxPython-4.2.0-cp310-cp310-linux_x86_64.whl - name: Install RIDE dependencies run: pip install -r requirements-dev.txt + git submodule update - name: Run Xvfb run: Xvfb :1 -noreset & - name: Test Install RIDE diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..8eb2cad0d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/robotide/preferences/configobj"] + path = src/robotide/preferences/configobj + url = https://github.com/DiffSK/configobj.git diff --git a/src/robotide/preferences/__init__.py b/src/robotide/preferences/__init__.py index c966db937..fe42d12c7 100644 --- a/src/robotide/preferences/__init__.py +++ b/src/robotide/preferences/__init__.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from .configobj.src.configobj import ConfigObj, ConfigObjError, Section, UnreprError from .editor import PreferenceEditor from .editors import GridEditorPreferences, TextEditorPreferences, TestRunnerPreferences from .general import DefaultPreferences diff --git a/src/robotide/preferences/configobj b/src/robotide/preferences/configobj new file mode 160000 index 000000000..961800486 --- /dev/null +++ b/src/robotide/preferences/configobj @@ -0,0 +1 @@ +Subproject commit 9618004862103d51741c8f40b55617c937e910a7 diff --git a/src/robotide/preferences/configobj.py b/src/robotide/preferences/configobj.py deleted file mode 100644 index 7ea505939..000000000 --- a/src/robotide/preferences/configobj.py +++ /dev/null @@ -1,2336 +0,0 @@ -# configobj.py -# A config file reader/writer that supports nested sections in config files. -# Copyright (C) 2005-2014: -# (name) : (email) -# Michael Foord: fuzzyman AT voidspace DOT org DOT uk -# Nicola Larosa: nico AT tekNico DOT net -# Rob Dennis: rdennis AT gmail DOT com -# Eli Courtwright: eli AT courtwright DOT org - -# This software is licensed under the terms of the BSD license. -# http://opensource.org/licenses/BSD-3-Clause - -# ConfigObj 5 - main repository for documentation and issue tracking: -# https://github.com/DiffSK/configobj - -import os -import re -import sys -from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE - -import six - -# imported lazily to avoid startup performance hit if it isn't used -compiler = None - -# A dictionary mapping BOM to -# the encoding to decode with, and what to set the -# encoding attribute to. -BOMS = { - BOM_UTF8: ('utf_8', None), - BOM_UTF16_BE: ('utf16_be', 'utf_16'), - BOM_UTF16_LE: ('utf16_le', 'utf_16'), - BOM_UTF16: ('utf_16', 'utf_16'), - } -# All legal variants of the BOM codecs. -# DEBUG: TODO: the list of aliases is not meant to be exhaustive, is there a better way? -BOM_LIST = { - 'utf_16': 'utf_16', - 'u16': 'utf_16', - 'utf16': 'utf_16', - 'utf-16': 'utf_16', - 'utf16_be': 'utf16_be', - 'utf_16_be': 'utf16_be', - 'utf-16be': 'utf16_be', - 'utf16_le': 'utf16_le', - 'utf_16_le': 'utf16_le', - 'utf-16le': 'utf16_le', - 'utf_8': 'utf_8', - 'u8': 'utf_8', - 'utf': 'utf_8', - 'utf8': 'utf_8', - 'utf-8': 'utf_8', - } - -# Map of encodings to the BOM to write. -BOM_SET = { - 'utf_8': BOM_UTF8, - 'utf_16': BOM_UTF16, - 'utf16_be': BOM_UTF16_BE, - 'utf16_le': BOM_UTF16_LE, - None: BOM_UTF8 - } - - -def match_utf8(encoding): - return BOM_LIST.get(encoding.lower()) == 'utf_8' - - -# Quote strings used for writing values -squot = "'%s'" -dquot = '"%s"' -noquot = "%s" -wspace_plus = ' \r\n\v\t\'"' -tsquot = '"""%s"""' -tdquot = "'''%s'''" - -# Sentinel for use in getattr calls to replace hasattr -MISSING = object() - -__all__ = ( - 'DEFAULT_INDENT_TYPE', - 'DEFAULT_INTERPOLATION', - 'ConfigObjError', - 'NestingError', - 'ParseError', - 'DuplicateError', - 'ConfigspecError', - 'ConfigObj', - 'Section', - 'SimpleVal', - 'InterpolationError', - 'InterpolationLoopError', - 'MissingInterpolationOption', - 'RepeatSectionError', - 'ReloadError', - 'UnreprError', - 'UnknownType', - 'flatten_errors', - 'get_extra_values' -) - -DEFAULT_INTERPOLATION = 'configparser' -DEFAULT_INDENT_TYPE = ' ' -MAX_INTERPOL_DEPTH = 10 - -OPTION_DEFAULTS = { - 'interpolation': True, - 'raise_errors': False, - 'list_values': True, - 'create_empty': False, - 'file_error': False, - 'configspec': None, - 'stringify': True, - # option may be set to one of ('', ' ', '\t') - 'indent_type': None, - 'encoding': None, - 'default_encoding': None, - 'unrepr': False, - 'write_empty_values': False, -} - -VALCANNOTQUOTED = 'Value "%s" cannot be safely quoted.' - - -class UnknownType(Exception): - pass - - -def unrepr(s): - if not s: - return s - - # this is supposed to be safe - import ast - return ast.literal_eval(s) - - -class ConfigObjError(SyntaxError): - """ - This is the base class for all errors that ConfigObj raises. - It is a subclass of SyntaxError. - """ - def __init__(self, message='', line_number=None, line=''): - self.line = line - self.line_number = line_number - SyntaxError.__init__(self, message) - - -class NestingError(ConfigObjError): - """ - This error indicates a level of nesting that doesn't match. - """ - - -class ParseError(ConfigObjError): - """ - This error indicates that a line is badly written. - It is neither a valid ``key = value`` line, - nor a valid section marker line. - """ - - -class ReloadError(IOError): - """ - A 'reload' operation failed. - This exception is a subclass of ``IOError``. - """ - def __init__(self): - IOError.__init__(self, 'reload failed, filename is not set.') - - -class DuplicateError(ConfigObjError): - """ - The keyword or section specified already exists. - """ - - -class ConfigspecError(ConfigObjError): - """ - An error occured whilst parsing a configspec. - """ - - -class InterpolationError(ConfigObjError): - """Base class for the two interpolation errors.""" - - -class InterpolationLoopError(InterpolationError): - """Maximum interpolation depth exceeded in string interpolation.""" - - def __init__(self, option): - InterpolationError.__init__( - self, - 'interpolation loop detected in value "%s".' % option) - - -class RepeatSectionError(ConfigObjError): - """ - This error indicates additional sections in a section with a - ``__many__`` (repeated) section. - """ - - -class MissingInterpolationOption(InterpolationError): - """A value specified for interpolation was missing.""" - def __init__(self, option): - msg = 'missing option "%s" in interpolation.' % option - InterpolationError.__init__(self, msg) - - -class UnreprError(ConfigObjError): - """An error parsing in unrepr mode.""" - - -class InterpolationEngine(object): - """ - A helper class to help perform string interpolation. - - This class is an abstract base class; its descendants perform - the actual work. - """ - - # compiled regexp to use in self.interpolate() - _KEYCRE = re.compile(r"%\(([^)]*)\)s") - _cookie = '%' - - def __init__(self, section): - # the Section instance that "owns" this engine - self.section = section - - def interpolate(self, key, value): - # short-cut - if self._cookie not in value: - return value - - def recursive_interpolate(kkey, vvalue, section, backtrail): - """The function that does the actual work. - - ``value``: the string we're trying to interpolate. - ``section``: the section in which that string was found - ``backtrail``: a dict to keep track of where we've been, - to detect and prevent infinite recursion loops - - This is similar to a depth-first-search algorithm. - """ - # Have we been here already? - if (kkey, section.name) in backtrail: - # Yes - infinite loop detected - raise InterpolationLoopError(kkey) - # Place a marker on our backtrail, so we won't come back here again - backtrail[(kkey, section.name)] = 1 - - # Now start the actual work - match = self._KEYCRE.search(vvalue) - while match: - # The actual parsing of the match is implementation-dependent, - # so delegate to our helper function - k, v, s = self._parse_match(match) - if k is None: - # That's the signal that no further interpolation is needed - replacement = v - else: - # Further interpolation may be needed to obtain final value - replacement = recursive_interpolate(k, v, s, backtrail) - # Replace the matched string with its final value - start, end = match.span() - vvalue = ''.join((vvalue[:start], replacement, vvalue[end:])) - new_search_start = start + len(replacement) - # Pick up the next interpolation key, if any, for next time - # through the while loop - match = self._KEYCRE.search(vvalue, new_search_start) - - # Now safe to come back here again; remove marker from backtrail - del backtrail[(kkey, section.name)] - - return vvalue - - # Back in interpolate(), all we have to do is kick off the recursive - # function with appropriate starting values - value = recursive_interpolate(key, value, self.section, {}) - return value - - def _fetch(self, key): - """Helper function to fetch values from owning section. - - Returns a 2-tuple: the value, and the section where it was found. - """ - # switch off interpolation before we try and fetch anything ! - save_interp = self.section.main.interpolation - self.section.main.interpolation = False - - # Start at section that "owns" this InterpolationEngine - current_section = self.section - while True: - # try the current section first - val = current_section.get(key) - if val is not None and not isinstance(val, Section): - break - # try "DEFAULT" next - val = current_section.get('DEFAULT', {}).get(key) - if val is not None and not isinstance(val, Section): - break - # move up to parent and try again - # top-level's parent is itself - if current_section.parent is current_section: - # reached top level, time to give up - break - current_section = current_section.parent - - # restore interpolation to previous value before returning - self.section.main.interpolation = save_interp - if val is None: - raise MissingInterpolationOption(key) - return val, current_section - - def _parse_match(self, match): - """Implementation-dependent helper function. - - Will be passed a match object corresponding to the interpolation - key we just found (e.g., "%(foo)s" or "$foo"). Should look up that - key in the appropriate config file section (using the ``_fetch()`` - helper function) and return a 3-tuple: (key, value, section) - - ``key`` is the name of the key we're looking for - ``value`` is the value found for that key - ``section`` is a reference to the section where it was found - - ``key`` and ``section`` should be None if no further - interpolation should be performed on the resulting value - (e.g., if we interpolated "$$" and returned "$"). - """ - raise NotImplementedError() - - -class ConfigParserInterpolation(InterpolationEngine): - """Behaves like ConfigParser.""" - _cookie = '%' - _KEYCRE = re.compile(r"%\(([^)]*)\)s") - - def _parse_match(self, match): - key = match.group(1) - value, section = self._fetch(key) - return key, value, section - - -class TemplateInterpolation(InterpolationEngine): - """Behaves like string.Template.""" - _cookie = '$' - _delimiter = '$' - _KEYCRE = re.compile(r""" - \$(?: - (?P\$) | # Two $ signs - (?P[_a-z][_a-z0-9]*) | # $name format - {(?P[^}]*)} # ${name} format - ) - """, re.IGNORECASE | re.VERBOSE) - - def _parse_match(self, match): - # Valid name (in or out of braces): fetch value from section - key = match.group('named') or match.group('braced') - if key is not None: - value, section = self._fetch(key) - return key, value, section - # Escaped delimiter (e.g., $$): return single delimiter - if match.group('escaped') is not None: - # Return None for key and section to indicate it's time to stop - return None, self._delimiter, None - # Anything else: ignore completely, just return it unchanged - return None, match.group(), None - - -interpolation_engines = { - 'configparser': ConfigParserInterpolation, - 'template': TemplateInterpolation, -} - - -def __newobj__(cls, *args): - # Hack for pickle - return cls.__new__(cls, *args) - - -class Section(dict): - """ - A dictionary-like object that represents a section in a config file. - - It does string interpolation if the 'interpolation' attribute - of the 'main' object is set to True. - - Interpolation is tried first from this object, then from the 'DEFAULT' - section of this object, next from the parent and its 'DEFAULT' section, - and so on until the main object is reached. - - A Section will behave like an ordered dictionary - following the - order of the ``scalars`` and ``sections`` attributes. - You can use this to change the order of members. - - Iteration follows the order: scalars, then sections. - """ - - def __setstate__(self, state): - dict.update(self, state[0]) - self.__dict__.update(state[1]) - - def __reduce__(self): - state = (dict(self), self.__dict__) - return __newobj__, (self.__class__,), state - - def __init__(self, parent, depth, main, indict=None, name=None): - """ - * parent is the section above - * depth is the depth level of this section - * main is the main ConfigObj - * indict is a dictionary to initialise the section with - """ - if indict is None: - indict = {} - dict.__init__(self) - # used for nesting level *and* interpolation - self.parent = parent - # used for the interpolation attribute - self.main = main - # level of nesting depth of this Section - self.depth = depth - # purely for information - self.name = name - # - self._initialise() - # we do this explicitly so that __setitem__ is used properly - # (rather than just passing to ``dict.__init__``) - for entry, value in indict.items(): - self[entry] = value - - def _initialise(self): - # the sequence of scalar values in this Section - self.scalars = [] - # the sequence of sections in this Section - self.sections = [] - # for comments :-) - self.comments = {} - self.inline_comments = {} - # the configspec - self.configspec = None - # for defaults - self.defaults = [] - self.default_values = {} - self.extra_values = [] - self._created = False - - def _interpolate(self, key, value): - try: - # do we already have an interpolation engine? - engine = self._interpolation_engine - except AttributeError: - # not yet: first time running _interpolate(), so pick the engine - name = self.main.interpolation - if name is True: # note that if name would be incorrect here - # backwards-compatibility: interpolation=True means use default - name = DEFAULT_INTERPOLATION - name = name.lower() # so that "Template", "template", etc. all work - class_ = interpolation_engines.get(name, None) - if class_ is None: - # invalid value for self.main.interpolation - self.main.interpolation = False - return value - else: - # save reference to engine, so we don't have to do this again - engine = self._interpolation_engine = class_(self) - # let the engine do the actual work - return engine.interpolate(key, value) - - def __getitem__(self, key): - """Fetch the item and do string interpolation.""" - val = dict.__getitem__(self, key) - if self.main.interpolation: - if isinstance(val, six.string_types): - return self._interpolate(key, val) - if isinstance(val, list): - def _check(entry): - if isinstance(entry, six.string_types): - return self._interpolate(key, entry) - return entry - new = [_check(entry) for entry in val] - if new != val: - return new - return val - - def __setitem__(self, key, value, uunrepr=False): - """ - Correctly set a value. - - Making dictionary values Section instances. - (We have to special case 'Section' instances - which are also dicts) - - Keys must be strings. - Values need only be strings (or lists of strings) if - ``main.stringify`` is set. - - ``unrepr`` must be set when setting a value to a dictionary, without creating a new subsection. - """ - if not isinstance(key, six.string_types): - raise ValueError('The key "%s" is not a string.' % key) - - # add the comment - if key not in self.comments: - self.comments[key] = [] - self.inline_comments[key] = '' - # remove the entry from defaults - if key in self.defaults: - self.defaults.remove(key) - # - if isinstance(value, Section): - if key not in self: - self.sections.append(key) - dict.__setitem__(self, key, value) - elif isinstance(value, dict) and not uunrepr: - # First create the new depth level, - # then create the section - if key not in self: - self.sections.append(key) - new_depth = self.depth + 1 - dict.__setitem__( - self, - key, - Section( - self, - new_depth, - self.main, - indict=value, - name=key)) - else: - if key not in self: - self.scalars.append(key) - if not self.main.stringify: - if isinstance(value, six.string_types): - print("") - elif isinstance(value, (list, tuple)): - for entry in value: - if not isinstance(entry, six.string_types): - raise TypeError('Value is not a string "%s".' % entry) - else: - raise TypeError('Value is not a string "%s".' % value) - dict.__setitem__(self, key, value) - - def __delitem__(self, key): - """Remove items from the sequence when deleting.""" - dict. __delitem__(self, key) - if key in self.scalars: - self.scalars.remove(key) - else: - self.sections.remove(key) - del self.comments[key] - del self.inline_comments[key] - - def get(self, key, default=None): - """A version of ``get`` that doesn't bypass string interpolation.""" - try: - return self[key] - except KeyError: - return default - - def update(self, indict): - """ - A version of update that uses our ``__setitem__``. - """ - for entry in indict: - self[entry] = indict[entry] - - def pop(self, key, default=MISSING): - """ - D.pop(k[,d]) -> v, remove specified key and return the corresponding value. - If key is not found, d is returned if given, otherwise KeyError is raised - """ - try: - val = self[key] - except KeyError: - if default is MISSING: - raise - val = default - else: - del self[key] - return val - - def popitem(self): - """Pops the first (key,val)""" - sequence = (self.scalars + self.sections) - if not sequence: - raise KeyError(": 'popitem(): dictionary is empty'") - key = sequence[0] - val = self[key] - del self[key] - return key, val - - def clear(self): - """ - A version of clear that also affects scalars/sections - Also clears comments and configspec. - - Leaves other attributes alone : - depth/main/parent are not affected - """ - dict.clear(self) - self.scalars = [] - self.sections = [] - self.comments = {} - self.inline_comments = {} - self.configspec = None - self.defaults = [] - self.extra_values = [] - - def setdefault(self, key, default=None): - """A version of setdefault that sets sequence if appropriate.""" - try: - return self[key] - except KeyError: - self[key] = default - return self[key] - - def items(self): - """D.items() -> list of D's (key, value) pairs, as 2-tuples""" - return list(zip((self.scalars + self.sections), list(self.values()))) - - def keys(self): - """D.keys() -> list of D's keys""" - return self.scalars + self.sections - - def values(self): - """D.values() -> list of D's values""" - return [self[key] for key in (self.scalars + self.sections)] - - def iteritems(self): - """D.iteritems() -> an iterator over the (key, value) items of D""" - return iter(list(self.items())) - - def iterkeys(self): - """D.iterkeys() -> an iterator over the keys of D""" - return iter((self.scalars + self.sections)) - - __iter__ = iterkeys - - def itervalues(self): - """D.itervalues() -> an iterator over the values of D""" - return iter(list(self.values())) - - def __repr__(self): - """x.__repr__() <==> repr(x)""" - def _getval(key): - try: - return self[key] - except MissingInterpolationOption: - return dict.__getitem__(self, key) - return '{%s}' % ', '.join([('%s: %s' % (repr(key), repr(_getval(key)))) - for key in (self.scalars + self.sections)]) - - __str__ = __repr__ - __str__.__doc__ = "x.__str__() <==> str(x)" - - # Extra methods - not in a normal dictionary - - def dict(self): - """ - Return a deepcopy of self as a dictionary. - - All members that are ``Section`` instances are recursively turned to - ordinary dictionaries - by calling their ``dict`` method. - - >>> n = a.dict() - >>> n == a - 1 - >>> n is a - 0 - """ - newdict = {} - for entry in self: - this_entry = self[entry] - if isinstance(this_entry, Section): - this_entry = this_entry.dict() - elif isinstance(this_entry, list): - # create a copy rather than a reference - this_entry = list(this_entry) - elif isinstance(this_entry, tuple): - # create a copy rather than a reference - this_entry = tuple(this_entry) - newdict[entry] = this_entry - return newdict - - def merge(self, indict): - """ - A recursive update - useful for merging config files. - - >>> a = '''[section1] - ... option1 = True - ... [[subsection]] - ... more_options = False - ... # end of file'''.splitlines() - >>> b = '''# File is user.ini - ... [section1] - ... option1 = False - ... # end of file'''.splitlines() - >>> c1 = ConfigObj(b) - >>> c2 = ConfigObj(a) - >>> c2.merge(c1) - >>> c2 - ConfigObj({'section1': {'option1': 'False', 'subsection': {'more_options': 'False'}}}) - """ - for key, val in list(indict.items()): - if key in self and isinstance(self[key], dict) and isinstance(val, dict): - self[key].merge(val) - else: - self[key] = val - - def rename(self, oldkey, newkey): - """ - Change a keyname to another, without changing position in sequence. - - Implemented so that transformations can be made on keys, - as well as on values. (used by encode and decode) - - Also renames comments. - """ - if oldkey in self.scalars: - the_list = self.scalars - elif oldkey in self.sections: - the_list = self.sections - else: - raise KeyError('Key "%s" not found.' % oldkey) - pos = the_list.index(oldkey) - # - val = self[oldkey] - dict.__delitem__(self, oldkey) - dict.__setitem__(self, newkey, val) - the_list.remove(oldkey) - the_list.insert(pos, newkey) - comm = self.comments[oldkey] - inline_comment = self.inline_comments[oldkey] - del self.comments[oldkey] - del self.inline_comments[oldkey] - self.comments[newkey] = comm - self.inline_comments[newkey] = inline_comment - - """ This is not used - def walk(self, function, raise_errors=True, call_on_sections=False, **keywargs): - ''' - Walk every member and call a function on the keyword and value. - - Return a dictionary of the return values - - If the function raises an exception, raise the errror - unless ``raise_errors=False``, in which case set the return value to - ``False``. - - Any unrecognised keyword arguments you pass to walk, will be pased on - to the function you pass in. - - Note: if ``call_on_sections`` is ``True`` then - on encountering a - subsection, *first* the function is called for the *whole* subsection, - and then recurses into its members. This means your function must be - able to handle strings, dictionaries and lists. This allows you - to change the key of subsections as well as for ordinary members. The - return value when called on the whole subsection has to be discarded. - - See the encode and decode methods for examples, including functions. - - .. admonition:: caution - - You can use ``walk`` to transform the names of members of a section - but you mustn't add or delete members. - - >>> config = '''[XXXXsection] - ... XXXXkey = XXXXvalue'''.splitlines() - >>> cfg = ConfigObj(config) - >>> cfg - ConfigObj({'XXXXsection': {'XXXXkey': 'XXXXvalue'}}) - >>> def transform(section, key): - ... val = section[key] - ... newkey = key.replace('XXXX', 'CLIENT1') - ... section.rename(key, newkey) - ... if isinstance(val, (tuple, list, dict)): - ... pass - ... else: - ... val = val.replace('XXXX', 'CLIENT1') - ... section[newkey] = val - >>> cfg.walk(transform, call_on_sections=True) - {'CLIENT1section': {'CLIENT1key': None}} - >>> cfg - ConfigObj({'CLIENT1section': {'CLIENT1key': 'CLIENT1value'}}) - ''' - out = {} - # scalars first - for i in range(len(self.scalars)): - entry = self.scalars[i] - try: - val = function(self, entry, **keywargs) - # bound again in case name has changed - entry = self.scalars[i] - out[entry] = val - except Exception: - if raise_errors: - raise - else: - entry = self.scalars[i] - out[entry] = False - # then sections - for i in range(len(self.sections)): - entry = self.sections[i] - if call_on_sections: - try: - function(self, entry, **keywargs) - except Exception: - if raise_errors: - raise - else: - entry = self.sections[i] - out[entry] = False - # bound again in case name has changed - entry = self.sections[i] - # previous result is discarded - out[entry] = self[entry].walk( - function, - raise_errors=raise_errors, - call_on_sections=call_on_sections, - **keywargs) - return out - - def as_bool(self, key): - ''' - Accepts a key as input. The corresponding value must be a string or - the objects (``True`` or 1) or (``False`` or 0). We allow 0 and 1 to - retain compatibility with Python 2.2. - - If the string is one of ``True``, ``On``, ``Yes``, or ``1`` it returns - ``True``. - - If the string is one of ``False``, ``Off``, ``No``, or ``0`` it returns - ``False``. - - ``as_bool`` is not case-sensitive. - - Any other input will raise a ``ValueError``. - - >>> a = ConfigObj() - >>> a['a'] = 'fish' - >>> a.as_bool('a') - Traceback (most recent call last): - ValueError: Value "fish" is neither True nor False - >>> a['b'] = 'True' - >>> a.as_bool('b') - 1 - >>> a['b'] = 'off' - >>> a.as_bool('b') - 0 - ''' - val = self[key] - if val == True: - return True - elif val == False: - return False - else: - try: - if not isinstance(val, six.string_types): - # DEBUG: TODO: Why do we raise a KeyError here? - raise KeyError() - else: - return self.main._bools[val.lower()] - except KeyError: - raise ValueError('Value "%s" is neither True nor False' % val) - - def as_int(self, key): - ''' - A convenience method which coerces the specified value to an integer. - - If the value is an invalid literal for ``int``, a ``ValueError`` will - be raised. - - >>> a = ConfigObj() - >>> a['a'] = 'fish' - >>> a.as_int('a') - Traceback (most recent call last): - ValueError: invalid literal for int() with base 10: 'fish' - >>> a['b'] = '1' - >>> a.as_int('b') - 1 - >>> a['b'] = '3.2' - >>> a.as_int('b') - Traceback (most recent call last): - ValueError: invalid literal for int() with base 10: '3.2' - ''' - return int(self[key]) - - def as_float(self, key): - ''' - A convenience method which coerces the specified value to a float. - - If the value is an invalid literal for ``float``, a ``ValueError`` will - be raised. - - >>> a = ConfigObj() - >>> a['a'] = 'fish' - >>> a.as_float('a') #doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ValueError: invalid literal for float(): fish - >>> a['b'] = '1' - >>> a.as_float('b') - 1.0 - >>> a['b'] = '3.2' - >>> a.as_float('b') #doctest: +ELLIPSIS - 3.2... - ''' - return float(self[key]) - - def as_list(self, key): - ''' - A convenience method which fetches the specified value, guaranteeing - that it is a list. - - >>> a = ConfigObj() - >>> a['a'] = 1 - >>> a.as_list('a') - [1] - >>> a['a'] = (1,) - >>> a.as_list('a') - [1] - >>> a['a'] = [1] - >>> a.as_list('a') - [1] - ''' - result = self[key] - if isinstance(result, (tuple, list)): - return list(result) - return [result] - """ - - def restore_default(self, key): - """ - Restore (and return) default value for the specified key. - - This method will only work for a ConfigObj that was created - with a configspec and has been validated. - - If there is no default value for this key, ``KeyError`` is raised. - """ - default = self.default_values[key] - dict.__setitem__(self, key, default) - if key not in self.defaults: - self.defaults.append(key) - return default - - def restore_defaults(self): - """ - Recursively restore default values to all members - that have them. - - This method will only work for a ConfigObj that was created - with a configspec and has been validated. - - It doesn't delete or modify entries without default values. - """ - for key in self.default_values: - self.restore_default(key) - - for section in self.sections: - self[section].restore_defaults() - - -class ConfigObj(Section): - """An object to read, create, and write config files.""" - - _keyword = re.compile(r'''^ # line start - (\s*) # indentation - ( # keyword - (?:"[^"]*")| # double quotes - (?:'[^']*')| # single quotes - (?:[^'"=].*?) # no quotes - ) - \s*=\s* # divider - (.*) # value (including list values and comments) - $ # line end - ''', re.VERBOSE) - - _sectionmarker = re.compile(r'''^ - (\s*) # 1: indentation - ((?:\[\s*)+) # 2: section marker open - ( # 3: section name open - (?:"\s*\S.*?\s*")| # at least one non-space with double quotes - (?:'\s*\S.*?\s*')| # at least one non-space with single quotes - (?:[^'"\s].*?) # at least one non-space unquoted - ) # section name close - ((?:\s*])+) # 4: section marker close - \s*(\#.*)? # 5: optional comment - $''', re.VERBOSE) - - # this regexp pulls list values out as a single string - # or single values and comments - # DEBUG: FIXME: this regex adds a '' to the end of comma terminated lists - # workaround in ``_handle_value`` - _valueexp = re.compile(r'''^ - (?: - (?: - ( - (?: - (?: - (?:"[^"]*")| # double quotes - (?:'[^']*')| # single quotes - (?:[^'",\#][^,\#]*?) # unquoted - ) - \s*,\s* # comma - )* # match all list items ending in a comma (if any) - ) - ( - (?:"[^"]*")| # double quotes - (?:'[^']*')| # single quotes - (?:[^'",\#\s][^,]*?)| # unquoted - (?:(? 1: - msg = "Parsing failed with several errors.\nFirst error %s" % info - error = ConfigObjError(msg) - else: - error = self._errors[0] - # set the errors attribute; it's a list of tuples: - # (error_type, message, line_number) - error.errors = self._errors - # set the config attribute - error.config = self - raise error - # delete private attributes - del self._errors - - if configspec is None: - self.configspec = None - else: - self._handle_configspec(configspec) - - def _initialise(self, options=None): - if options is None: - options = OPTION_DEFAULTS - - # initialise a few variables - self.filename = None - self._errors = [] - self.raise_errors = options['raise_errors'] - self.interpolation = options['interpolation'] - self.list_values = options['list_values'] - self.create_empty = options['create_empty'] - self.file_error = options['file_error'] - self.stringify = options['stringify'] - self.indent_type = options['indent_type'] - self.encoding = options['encoding'] - self.default_encoding = options['default_encoding'] - self.BOM = False - self.newlines = None - self.write_empty_values = options['write_empty_values'] - self.unrepr = options['unrepr'] - - self.initial_comment = [] - self.final_comment = [] - self.configspec = None - - if self._inspec: - self.list_values = False - - # Clear section attributes as well - Section._initialise(self) - - def __repr__(self): - def _getval(key): - try: - return self[key] - except MissingInterpolationOption: - return dict.__getitem__(self, key) - return ('ConfigObj({%s})' % - ', '.join([('%s: %s' % (repr(key), repr(_getval(key)))) for key in (self.scalars + self.sections)])) - - def _handle_bom(self, infile): - """ - Handle any BOM, and decode if necessary. - - If an encoding is specified, that *must* be used - but the BOM should - still be removed (and the BOM attribute set). - - (If the encoding is wrongly specified, then a BOM for an alternative - encoding won't be discovered or removed.) - - If an encoding is not specified, UTF8 or UTF16 BOM will be detected and - removed. The BOM attribute will be set. UTF16 will be decoded to - unicode. - - NOTE: This method must not be called with an empty ``infile``. - - Specifying the *wrong* encoding is likely to cause a - ``UnicodeDecodeError``. - - ``infile`` must always be returned as a list of lines, but may be - passed in as a single string. - """ - - if (self.encoding is not None) and (self.encoding.lower() not in BOM_LIST): - # No need to check for a BOM the encoding specified doesn't have one just decode - return self._decode(infile, self.encoding) - - if isinstance(infile, (list, tuple)): - line = infile[0] - else: - line = infile - - if isinstance(line, six.text_type): - # it's already decoded and there's no need to do anything - # else, just use the _decode utility method to handle - # listifying appropriately - return self._decode(infile, self.encoding) - - if self.encoding is not None: - # encoding explicitly supplied - # And it could have an associated BOM - # DEBUG: TODO: if encoding is just UTF16 - we ought to check for both - # big endian and little endian versions. - enc = BOM_LIST[self.encoding.lower()] - if enc == 'utf_16': - # For UTF16 we try big endian and little endian - for _bom_, (encoding, final_encoding) in list(BOMS.items()): - if not final_encoding: - # skip UTF8 - continue - if infile.startswith(_bom_): - # BOM discovered - # self.BOM = True - # Don't need to remove BOM - return self._decode(infile, encoding) - - # If we get this far, will *probably* raise a DecodeError - # As it doesn't appear to start with a BOM - return self._decode(infile, self.encoding) - - # Must be UTF8 - _bom_ = BOM_SET[enc] - if not line.startswith(_bom_): - return self._decode(infile, self.encoding) - - newline = line[len(_bom_):] - - # BOM removed - if isinstance(infile, (list, tuple)): - infile[0] = newline - else: - infile = newline - self.BOM = True - return self._decode(infile, self.encoding) - - # No encoding specified - so we need to check for UTF8/UTF16 - for _bom_, (encoding, final_encoding) in list(BOMS.items()): - if not isinstance(line, six.binary_type) or not line.startswith(_bom_): - # didn't specify a BOM, or it's not a bytestring - continue - else: - # BOM discovered - self.encoding = final_encoding - if not final_encoding: - self.BOM = True - # UTF8 - # remove BOM - newline = line[len(_bom_):] - if isinstance(infile, (list, tuple)): - infile[0] = newline - else: - infile = newline - # UTF-8 - if isinstance(infile, six.text_type): - return infile.splitlines(True) - elif isinstance(infile, six.binary_type): - return infile.decode('utf-8').splitlines(True) - else: - return self._decode(infile, 'utf-8') - # UTF16 - have to decode - return self._decode(infile, encoding) - - if six.PY2 and isinstance(line, str): - # don't actually do any decoding, since we're on python 2 and - # returning a bytestring is fine - return self._decode(infile, None) - # No BOM discovered and no encoding specified, default to UTF-8 - if isinstance(infile, six.binary_type): - return infile.decode('utf-8').splitlines(True) - else: - return self._decode(infile, 'utf-8') - - def _a_to_u(self, a_string): - """Decode ASCII strings to unicode if a 'self.encoding' is specified.""" - if isinstance(a_string, six.binary_type) and self.encoding: - return a_string.decode(self.encoding) - else: - return a_string - - @staticmethod - def _decode(infile, encoding): - """ - Decode infile to unicode. Using the specified encoding. - - if is a string, it also needs converting to a list. - """ - if isinstance(infile, six.string_types): - return infile.splitlines(True) - if isinstance(infile, six.binary_type): - # NOTE: Could raise a ``UnicodeDecodeError`` - if encoding: - return infile.decode(encoding).splitlines(True) - else: - return infile.splitlines(True) - - if encoding: - for i, line in enumerate(infile): - if isinstance(line, six.binary_type): - # NOTE: The isinstance test here handles mixed lists of unicode/string - # But the 'decode' will break on any non-string values - # Or could raise a ``UnicodeDecodeError`` - infile[i] = line.decode(encoding) - return infile - - def _decode_element(self, line): - """Decode element to unicode if necessary.""" - if isinstance(line, six.binary_type) and self.default_encoding: - return line.decode(self.default_encoding) - else: - return line - - # DEBUG: TODO: this may need to be modified - @staticmethod - def _str(value): - """ - Used by ``stringify`` within validate, to turn non-string values - into strings. - """ - if not isinstance(value, six.string_types): - # intentially 'str' because it's just whatever the "normal" - # string type is for the python version we're dealing with - return str(value) - else: - return value - - def _parse(self, infile): - """Actually parse the config file.""" - temp_list_values = self.list_values - if self.unrepr: - self.list_values = False - - comment_list = [] - done_start = False - this_section = self - maxline = len(infile) - 1 - cur_index = -1 - reset_comment = False - - while cur_index < maxline: - if reset_comment: - comment_list = [] - cur_index += 1 - line = infile[cur_index] - sline = line.strip() - # do we have anything on the line ? - if not sline or sline.startswith('#'): - reset_comment = False - comment_list.append(line) - continue - - if not done_start: - # preserve initial comment - self.initial_comment = comment_list - comment_list = [] - done_start = True - - reset_comment = True - # first we check if it's a section marker - mat = self._sectionmarker.match(line) - if mat is not None: - # is a section line - (indent, sect_open, sect_name, sect_close, comment) = mat.groups() - if indent and (self.indent_type is None): - self.indent_type = indent - cur_depth = sect_open.count('[') - if cur_depth != sect_close.count(']'): - self._handle_error("Cannot compute the section depth", - NestingError, infile, cur_index) - continue - - if cur_depth < this_section.depth: - # the new section is dropping back to a previous level - try: - parent = self._match_depth(this_section, - cur_depth).parent - except SyntaxError: - self._handle_error("Cannot compute nesting level", - NestingError, infile, cur_index) - continue - elif cur_depth == this_section.depth: - # the new section is a sibling of the current section - parent = this_section.parent - elif cur_depth == this_section.depth + 1: - # the new section is a child the current section - parent = this_section - else: - self._handle_error("Section too nested", - NestingError, infile, cur_index) - continue - - sect_name = self._unquote(sect_name) - if sect_name in parent: - self._handle_error('Duplicate section name', - DuplicateError, infile, cur_index) - continue - - # create the new section - this_section = Section( - parent, - cur_depth, - self, - name=sect_name) - parent[sect_name] = this_section - parent.inline_comments[sect_name] = comment - parent.comments[sect_name] = comment_list - continue - # - # it's not a section marker, - # so it should be a valid ``key = value`` line - mat = self._keyword.match(line) - if mat is None: - self._handle_error( - 'Invalid line ({0!r}) (matched as neither section nor keyword)'.format(line), - ParseError, infile, cur_index) - else: - # is a keyword value - # value will include any inline comment - (indent, key, value) = mat.groups() - if indent and (self.indent_type is None): - self.indent_type = indent - # check for a multiline value - if value[:3] in ['"""', "'''"]: - try: - value, comment, cur_index = self._multiline( - value, infile, cur_index, maxline) - except SyntaxError: - self._handle_error( - 'Parse error in multiline value', - ParseError, infile, cur_index) - continue - else: - if self.unrepr: - comment = '' - try: - value = unrepr(value) - except Exception as e: - if type(e) == UnknownType: - msg = 'Unknown name or type in value' - else: - msg = 'Parse error from unrepr-ing multiline value' - self._handle_error(msg, UnreprError, infile, cur_index) - continue - else: - if self.unrepr: - comment = '' - try: - value = unrepr(value) - except Exception as e: - if isinstance(e, UnknownType): - msg = 'Unknown name or type in value' - else: - msg = 'Parse error from unrepr-ing value' - self._handle_error(msg, UnreprError, infile, cur_index) - continue - else: - # extract comment and lists - try: - (value, comment) = self._handle_value(value) - except SyntaxError: - self._handle_error( - 'Parse error in value', - ParseError, infile, cur_index) - continue - # - key = self._unquote(key) - if key in this_section: - self._handle_error( - 'Duplicate keyword name', - DuplicateError, infile, cur_index) - continue - # add the key. - # we set unrepr because if we have got this far we will never - # be creating a new section - this_section.__setitem__(key, value, uunrepr=True) - this_section.inline_comments[key] = comment - this_section.comments[key] = comment_list - # - if self.indent_type is None: - # no indentation used, set the type accordingly - self.indent_type = '' - - # preserve the final comment - if not self and not self.initial_comment: - self.initial_comment = comment_list - elif not reset_comment: - self.final_comment = comment_list - self.list_values = temp_list_values - - @staticmethod - def _match_depth(sect, depth): - """ - Given a section and a depth level, walk back through the sections - parents to see if the depth level matches a previous section. - - Return a reference to the right section, - or raise a SyntaxError. - """ - while depth < sect.depth: - if sect is sect.parent: - # we've reached the top level already - raise SyntaxError() - sect = sect.parent - if sect.depth == depth: - return sect - # shouldn't get here - raise SyntaxError() - - def _handle_error(self, text, error_class, infile, cur_index): - """ - Handle an error according to the error settings. - - Either raise the error or store it. - The error will have occured at ``cur_index`` - """ - line = infile[cur_index] - cur_index += 1 - message = '{0} at line {1}.'.format(text, cur_index) - error = error_class(message, cur_index, line) - if self.raise_errors: - # raise the error - parsing stops here - raise error - # store the error - # reraise when parsing has finished - self._errors.append(error) - - @staticmethod - def _unquote(value): - """Return an unquoted version of a value""" - if not value: - # should only happen during parsing of lists - raise SyntaxError - if (value[0] == value[-1]) and (value[0] in ('"', "'")): - value = value[1:-1] - return value - - def _quote(self, value, multiline=True): - """ - Return a safely quoted version of a value. - - Raise a ConfigObjError if the value cannot be safely quoted. - If multiline is ``True`` (default) then use triple quotes - if necessary. - - * Don't quote values that don't need it. - * Recursively quote members of a list and return a comma joined list. - * Multiline is ``False`` for lists. - * Obey list syntax for empty and single member lists. - - If ``list_values=False`` then the value is only quoted if it contains - a ``\\n`` (is multiline) or '#'. - - If ``write_empty_values`` is set, and the value is an empty string, it - won't be quoted. - """ - if multiline and self.write_empty_values and value == '': - # Only if multiline is set, so that it is used for values not - # keys, and not values that are part of a list - return '' - - if multiline and isinstance(value, (list, tuple)): - if not value: - return ',' - elif len(value) == 1: - return self._quote(value[0], multiline=False) + ',' - return ', '.join([self._quote(val, multiline=False) - for val in value]) - if not isinstance(value, six.string_types): - if self.stringify: - # intentially 'str' because it's just whatever the "normal" - # string type is for the python version we're dealing with - value = str(value) - else: - raise TypeError('Value "%s" is not a string.' % value) - - if not value: - return '""' - - no_lists_no_quotes = not self.list_values and '\n' not in value and '#' not in value - need_triple = multiline and ((("'" in value) and ('"' in value)) or ('\n' in value)) - hash_triple_quote = multiline and not need_triple and ("'" in value) and ('"' in value) and ('#' in value) - check_for_single = (no_lists_no_quotes or not need_triple) and not hash_triple_quote - - if check_for_single: - if not self.list_values: - # we don't quote if ``list_values=False`` - quot = noquot - # for normal values either single or double quotes will do - elif '\n' in value: - # will only happen if multiline is off - e.g. '\n' in key - raise ConfigObjError(VALCANNOTQUOTED % value) - elif ((value[0] not in wspace_plus) and - (value[-1] not in wspace_plus) and - (',' not in value)): - quot = noquot - else: - quot = self._get_single_quote(value) - else: - # if value has '\n' or "'" *and* '"', it will need triple quotes - quot = self._get_triple_quote(value) - - if quot == noquot and '#' in value and self.list_values: - quot = self._get_single_quote(value) - - return quot % value - - @staticmethod - def _get_single_quote(value): - if ("'" in value) and ('"' in value): - raise ConfigObjError(VALCANNOTQUOTED % value) - elif '"' in value: - quot = squot - else: - quot = dquot - return quot - - @staticmethod - def _get_triple_quote(value): - if (value.find('"""') != -1) and (value.find("'''") != -1): - raise ConfigObjError(VALCANNOTQUOTED % value) - if value.find('"""') == -1: - quot = tdquot - else: - quot = tsquot - return quot - - def _handle_value(self, value): - """ - Given a value string, unquote, remove comment, - handle lists. (including empty and single member lists) - """ - if self._inspec: - # Parsing a configspec so don't handle comments - return value, '' - # do we look for lists in values ? - if not self.list_values: - mat = self._nolistvalue.match(value) - if mat is None: - raise SyntaxError() - # NOTE: we don't unquote here - return mat.groups() - # - mat = self._valueexp.match(value) - if mat is None: - # the value is badly constructed, probably badly quoted, - # or an invalid list - raise SyntaxError() - (list_values, single, empty_list, comment) = mat.groups() - if (list_values == '') and (single is None): - # change this if you want to accept empty values - raise SyntaxError() - # NOTE: note there is no error handling from here if the regex - # is wrong: then incorrect values will slip through - if empty_list is not None: - # the single comma - meaning an empty list - return [], comment - if single is not None: - # handle empty values - if list_values and not single: - # DEBUG: FIXME: the '' is a workaround because our regex now matches - # '' at the end of a list if it has a trailing comma - single = None - else: - single = single or '""' - single = self._unquote(single) - if list_values == '': - # not a list value - return single, comment - the_list = self._listvalueexp.findall(list_values) - the_list = [self._unquote(val) for val in the_list] - if single is not None: - the_list += [single] - return the_list, comment - - def _multiline(self, value, infile, cur_index, maxline): - """Extract the value, where we are in a multiline situation.""" - quot = value[:3] - newvalue = value[3:] - single_line = self._triple_quote[quot][0] - multi_line = self._triple_quote[quot][1] - mat = single_line.match(value) - if mat is not None: - retval = list(mat.groups()) - retval.append(cur_index) - return retval - elif newvalue.find(quot) != -1: - # somehow the triple quote is missing - raise SyntaxError() - # - while cur_index < maxline: - cur_index += 1 - newvalue += '\n' - line = infile[cur_index] - if line.find(quot) == -1: - newvalue += line - else: - # end of multiline, process it - break - else: - # we've got to the end of the config, oops... - raise SyntaxError() - mat = multi_line.match(line) - if mat is None: - # a badly formed line - raise SyntaxError() - (value, comment) = mat.groups() - return newvalue + value, comment, cur_index - - def _handle_configspec(self, configspec): - """Parse the configspec.""" - # DEBUG: FIXME: Should we check that the configspec was created with the - # correct settings ? (i.e. ``list_values=False``) - if not isinstance(configspec, ConfigObj): - try: - configspec = ConfigObj(configspec, - raise_errors=True, - file_error=True, - _inspec=True) - except ConfigObjError as e: - # DEBUG: FIXME: Should these errors have a reference - # to the already parsed ConfigObj ? - raise ConfigspecError('Parsing configspec failed: %s' % e) - except IOError as e: - raise IOError('Reading configspec failed: %s' % e) - - self.configspec = configspec - - @staticmethod - def _set_configspec(section, copy): - """ - Called by validate. Handles setting the configspec on subsections - including sections to be validated by __many__ - """ - configspec = section.configspec - many = configspec.get('__many__') - if isinstance(many, dict): - for entry in section.sections: - if entry not in configspec: - section[entry].configspec = many - - for entry in configspec.sections: - if entry == '__many__': - continue - if entry not in section: - section[entry] = {} - section[entry]._created = True - if copy: - # copy comments - section.comments[entry] = configspec.comments.get(entry, []) - section.inline_comments[entry] = configspec.inline_comments.get(entry, '') - - # Could be a scalar when we expect a section - if isinstance(section[entry], Section): - section[entry].configspec = configspec[entry] - - def _write_line(self, indent_string, entry, this_entry, comment): - """Write an individual line, for the write method""" - # NOTE: the calls to self._quote here handles non-StringType values. - if not self.unrepr: - val = self._decode_element(self._quote(this_entry)) - else: - val = repr(this_entry) - return '%s%s%s%s%s' % (indent_string, - self._decode_element(self._quote(entry, multiline=False)), - self._a_to_u(' = '), - val, - self._decode_element(comment)) - - def _write_marker(self, indent_string, depth, entry, comment): - """Write a section marker line""" - return '%s%s%s%s%s' % (indent_string, - self._a_to_u('[' * depth), - self._quote(self._decode_element(entry), multiline=False), - self._a_to_u(']' * depth), - self._decode_element(comment)) - - def _handle_comment(self, comment): - """Deal with a comment.""" - if not comment: - return '' - start = self.indent_type - if not comment.startswith('#'): - start += self._a_to_u(' # ') - return start + comment - - # Public methods - - def write(self, outfile=None, section=None): - """ - Write the current ConfigObj as a file - - tekNico: FIXME: use StringIO instead of real files - - >>> filename = a.filename - >>> a.filename = 'test.ini' - >>> a.write() - >>> a.filename = filename - >>> a == ConfigObj('test.ini', raise_errors=True) - 1 - >>> import os - >>> os.remove('test.ini') - """ - if self.indent_type is None: - # this can be true if initialised from a dictionary - self.indent_type = DEFAULT_INDENT_TYPE - - out = [] - cs = self._a_to_u('#') - csp = self._a_to_u('# ') - if section is None: - int_val = self.interpolation - self.interpolation = False - section = self - for line in self.initial_comment: - line = self._decode_element(line) - stripped_line = line.strip() - if stripped_line and not stripped_line.startswith(cs): - line = csp + line - out.append(line) - - indent_string = self.indent_type * section.depth - for entry in (section.scalars + section.sections): - if entry in section.defaults: - # don't write out default values - continue - for comment_line in section.comments[entry]: - comment_line = self._decode_element(comment_line.lstrip()) - if comment_line and not comment_line.startswith(cs): - comment_line = csp + comment_line - out.append(indent_string + comment_line) - this_entry = section[entry] - comment = self._handle_comment(section.inline_comments[entry]) - - if isinstance(this_entry, Section): - # a section - out.append(self._write_marker( - indent_string, - this_entry.depth, - entry, - comment)) - out.extend(self.write(section=this_entry)) - else: - out.append(self._write_line( - indent_string, - entry, - this_entry, - comment)) - - if section is self: - for line in self.final_comment: - line = self._decode_element(line) - stripped_line = line.strip() - if stripped_line and not stripped_line.startswith(cs): - line = csp + line - out.append(line) - self.interpolation = int_val - - if section is not self: - return out - - if (self.filename is None) and (outfile is None): - # output a list of lines - # might need to encode - # NOTE: This will *screw* UTF16, each line will start with the BOM - if self.encoding is not None: - out = [ll.encode(self.encoding) for ll in out] - if self.BOM and ((self.encoding is None) or (BOM_LIST.get(self.encoding.lower()) == 'utf_8')): - # Add the UTF8 BOM - if not out: - out.append('') - out[0] = BOM_UTF8 + out[0] - return out - - # Turn the list to a string, joined with correct newlines - newline = self.newlines or os.linesep - if (getattr(outfile, 'mode', None) is not None and outfile.mode == 'w' and sys.platform == 'win32' and - newline == '\r\n'): - # Windows specific hack to avoid writing '\r\r\n' - newline = '\n' - output = self._a_to_u(newline).join(out) - if not output.endswith(newline): - output += newline - - if isinstance(output, six.binary_type): - output_bytes = output - else: - output_bytes = output.encode(self.encoding or - self.default_encoding or - 'utf-8') # DEBUG was 'ascii' - if self.BOM and ((self.encoding is None) or match_utf8(self.encoding)): - # Add the UTF8 BOM - output_bytes = BOM_UTF8 + output_bytes - - if outfile is not None: - outfile.write(output_bytes) - else: - with open(self.filename, 'wb') as h: - h.write(output_bytes) - - def validate(self, validator, preserve_errors=False, copy=False, - section=None): - """ - Test the ConfigObj against a configspec. - - It uses the ``validator`` object from *validate.py*. - - To run ``validate`` on the current ConfigObj, call: :: - - test = config.validate(validator) - - (Normally having previously passed in the configspec when the ConfigObj - was created - you can dynamically assign a dictionary of checks to the - ``configspec`` attribute of a section though). - - It returns ``True`` if everything passes, or a dictionary of - pass/fails (True/False). If every member of a subsection passes, it - will just have the value ``True``. (It also returns ``False`` if all - members fail). - - In addition, it converts the values from strings to their native - types if their checks pass (and ``stringify`` is set). - - If ``preserve_errors`` is ``True`` (``False`` is default) then instead - of a marking a fail with a ``False``, it will preserve the actual - exception object. This can contain info about the reason for failure. - For example the ``VdtValueTooSmallError`` indicates that the value - supplied was too small. If a value (or section) is missing it will - still be marked as ``False``. - - You must have the validate module to use ``preserve_errors=True``. - - You can then use the ``flatten_errors`` function to turn your nested - results dictionary into a flattened list of failures - useful for - displaying meaningful error messages. - """ - if section is None: - if self.configspec is None: - raise ValueError('No configspec supplied.') - - section = self - - if copy: - section.initial_comment = section.configspec.initial_comment - section.final_comment = section.configspec.final_comment - section.encoding = section.configspec.encoding - section.BOM = section.configspec.BOM - section.newlines = section.configspec.newlines - section.indent_type = section.configspec.indent_type - - # - # section.default_values.clear() #?? - configspec = section.configspec - self._set_configspec(section, copy) - - def validate_entry(entry, spec, val, missing, ret_true, ret_false): - section.default_values.pop(entry, None) - - try: - section.default_values[entry] = validator.get_default_value(configspec[entry]) - except (KeyError, AttributeError, validator.base_error_class): - # No default, bad default or validator has no 'get_default_value' - # (e.g. SimpleVal) - pass - - try: - check = validator.check(spec, - val, - missing=missing - ) - except validator.base_error_class as e: - if not preserve_errors: - out[entry] = False - else: - # preserve the error - out[entry] = e - ret_false = False - ret_true = False - else: - ret_false = False - out[entry] = True - if self.stringify or missing: - # if we are doing type conversion - # or the value is a supplied default - if not self.stringify: - if isinstance(check, (list, tuple)): - # preserve lists - check = [self._str(item) for item in check] - elif missing and check is None: - # convert the None from a default to a '' - check = '' - else: - check = self._str(check) - if (check != val) or missing: - section[entry] = check - if not copy and missing and entry not in section.defaults: - section.defaults.append(entry) - return ret_true, ret_false - - # - out = {} - ret_true = True - ret_false = True - - unvalidated = [k for k in section.scalars if k not in configspec] - incorrect_sections = [k for k in configspec.sections if k in section.scalars] - incorrect_scalars = [k for k in configspec.scalars if k in section.sections] - - for entry in configspec.scalars: - if entry in ('__many__', '___many___'): - # reserved names - continue - if (entry not in section.scalars) or (entry in section.defaults): - # missing entries - # or entries from defaults - missing = True - val = None - if copy and entry not in section.scalars: - # copy comments - section.comments[entry] = ( - configspec.comments.get(entry, [])) - section.inline_comments[entry] = ( - configspec.inline_comments.get(entry, '')) - # - else: - missing = False - val = section[entry] - - ret_true, ret_false = validate_entry(entry, configspec[entry], val, - missing, ret_true, ret_false) - - many = None - if '__many__' in configspec.scalars: - many = configspec['__many__'] - elif '___many___' in configspec.scalars: - many = configspec['___many___'] - - if many is not None: - for entry in unvalidated: - val = section[entry] - ret_true, ret_false = validate_entry(entry, many, val, False, - ret_true, ret_false) - unvalidated = [] - - for entry in incorrect_scalars: - ret_true = False - if not preserve_errors: - out[entry] = False - else: - ret_false = False - msg = 'Value %r was provided as a section' % entry - out[entry] = validator.base_error_class(msg) - for entry in incorrect_sections: - ret_true = False - if not preserve_errors: - out[entry] = False - else: - ret_false = False - msg = 'Section %r was provided as a single value' % entry - out[entry] = validator.base_error_class(msg) - - # Missing sections will have been created as empty ones when the - # configspec was read. - for entry in section.sections: - # DEBUG: FIXME: this means DEFAULT is not copied in copy mode - if section is self and entry == 'DEFAULT': - continue - if section[entry].configspec is None: - unvalidated.append(entry) - continue - if copy: - section.comments[entry] = configspec.comments.get(entry, []) - section.inline_comments[entry] = configspec.inline_comments.get(entry, '') - check = self.validate(validator, preserve_errors=preserve_errors, copy=copy, section=section[entry]) - out[entry] = check - if check is False: - ret_true = False - elif check is True: - ret_false = False - else: - ret_true = False - - section.extra_values = unvalidated - if preserve_errors and not section._created: - # If the section wasn't created (i.e. it wasn't missing) - # then we can't return False, we need to preserve errors - ret_false = False - # - if ret_false and preserve_errors and out: - # If we are preserving errors, but all - # the failures are from missing sections / values - # then we can return False. Otherwise, there is a - # real failure that we need to preserve. - ret_false = not any(out.values()) - if ret_true: - return True - elif ret_false: - return False - return out - - def reset(self): - """Clear ConfigObj instance and restore to 'freshly created' state.""" - self.clear() - self._initialise() - # DEBUG: FIXME: Should be done by '_initialise', but ConfigObj constructor (and reload) - # requires an empty dictionary - self.configspec = None - # Just to be sure ;-) - self._original_configspec = None - - def reload(self): - """ - Reload a ConfigObj from file. - - This method raises a ``ReloadError`` if the ConfigObj doesn't have - a filename attribute pointing to a file. - """ - if not isinstance(self.filename, six.string_types): - raise ReloadError() - - filename = self.filename - current_options = {} - for entry in OPTION_DEFAULTS: - if entry == 'configspec': - continue - current_options[entry] = getattr(self, entry) - - configspec = self._original_configspec - current_options['configspec'] = configspec - - self.clear() - self._initialise(current_options) - self._load(filename, configspec) - - -class SimpleVal(object): - """ - A simple validator. - Can be used to check that all members expected are present. - - To use it, provide a configspec with all your members in (the value given - will be ignored). Pass an instance of ``SimpleVal`` to the ``validate`` - method of your ``ConfigObj``. ``validate`` will return ``True`` if all - members are present, or a dictionary with True/False meaning - present/missing. (Whole missing sections will be replaced with ``False``) - """ - - def __init__(self): - self.base_error_class = ConfigObjError - - def check(self, check, member, missing=False): - """A dummy check method, always returns the value unchanged.""" - _ = check - if missing: - raise self.base_error_class() - return member - - -def flatten_errors(cfg, res, levels=None, results=None): - """ - An example function that will turn a nested dictionary of results - (as returned by ``ConfigObj.validate``) into a flat list. - - ``cfg`` is the ConfigObj instance being checked, ``res`` is the results - dictionary returned by ``validate``. - - (This is a recursive function, so you shouldn't use the ``levels`` or - ``results`` arguments - they are used by the function.) - - Returns a list of keys that failed. Each member of the list is a tuple:: - - ([list of sections...], key, result) - - If ``validate`` was called with ``preserve_errors=False`` (the default) - then ``result`` will always be ``False``. - - *list of sections* is a flattened list of sections that the key was found - in. - - If the section was missing (or a section was expected and a scalar provided - - or vice-versa) then key will be ``None``. - - If the value (or section) was missing then ``result`` will be ``False``. - - If ``validate`` was called with ``preserve_errors=True`` and a value - was present, but failed the check, then ``result`` will be the exception - object returned. You can use this as a string that describes the failure. - - For example *The value "3" is of the wrong type*. - """ - if levels is None: - # first time called - levels = [] - results = [] - if res is True: - return sorted(results) - if res is False or isinstance(res, Exception): - results.append((levels[:], None, res)) - if levels: - levels.pop() - return sorted(results) - for (key, val) in list(res.items()): - if val is True: - continue - if isinstance(cfg.get(key), dict): - # Go down one level - levels.append(key) - flatten_errors(cfg[key], val, levels, results) - continue - results.append((levels[:], key, val)) - # - # Go up one level - if levels: - levels.pop() - # - return sorted(results) - - -def get_extra_values(conf, _prepend=()): - """ - Find all the values and sections not in the configspec from a validated - ConfigObj. - - ``get_extra_values`` returns a list of tuples where each tuple represents - either an extra section, or an extra value. - - The tuples contain two values, a tuple representing the section the value - is in and the name of the extra values. For extra values in the top level - section the first member will be an empty tuple. For values in the 'foo' - section the first member will be ``('foo',)``. For members in the 'bar' - subsection of the 'foo' section the first member will be ``('foo', 'bar')``. - - NOTE: If you call ``get_extra_values`` on a ConfigObj instance that hasn't - been validated it will return an empty list. - """ - out = [] - - out.extend([(_prepend, name) for name in conf.extra_values]) - for name in conf.sections: - if name not in conf.extra_values: - out.extend(get_extra_values(conf[name], _prepend + (name,))) - return out - - -"""*A programming language is a medium of expression.* - Paul Graham""" diff --git a/src/robotide/preferences/managesettingsdialog.py b/src/robotide/preferences/managesettingsdialog.py index 7c280cd04..1d6e3f25a 100644 --- a/src/robotide/preferences/managesettingsdialog.py +++ b/src/robotide/preferences/managesettingsdialog.py @@ -18,7 +18,8 @@ from wx import Colour from ..widgets import RIDEDialog, VerticalSizer -from .configobj import ConfigObj, UnreprError +# from .configobj import ConfigObj, UnreprError +from . import ConfigObj, UnreprError from .settings import ConfigurationError, _Section, initialize_settings from ..context import SETTINGS_DIRECTORY @@ -117,7 +118,7 @@ def on_save(self, event): def load_and_merge(self, user_path): try: - nnew_settings = ConfigObj(user_path, unrepr=True) + nnew_settings = ConfigObj(user_path, encoding='UTF-8', unrepr=True) mysection = nnew_settings.get(self._section) if not mysection: mysection = nnew_settings['Plugins'].get(self._section) diff --git a/src/robotide/preferences/settings.py b/src/robotide/preferences/settings.py index 23f364c66..688b4b432 100644 --- a/src/robotide/preferences/settings.py +++ b/src/robotide/preferences/settings.py @@ -17,7 +17,8 @@ import shutil from ..context import SETTINGS_DIRECTORY, LIBRARY_XML_DIRECTORY, EXECUTABLE -from .configobj import ConfigObj, ConfigObjError, Section, UnreprError +# from .configobj import ConfigObj, ConfigObjError, Section, UnreprError +from . import ConfigObj, ConfigObjError, Section, UnreprError from .excludes_class import Excludes from ..publish import RideSettingsChanged @@ -78,11 +79,11 @@ class SettingsMigrator(object): SETTINGS_VERSION = 'settings_version' def __init__(self, default_path, user_path): - self._default_settings = ConfigObj(default_path, unrepr=True) + self._default_settings = ConfigObj(default_path, encoding='UTF-8', unrepr=True) self._user_path = user_path # print("DEBUG: Settings migrator 1: %s\ndefault_path %s" % (self._default_settings.__repr__(), default_path)) try: - self._old_settings = ConfigObj(user_path, unrepr=True) + self._old_settings = ConfigObj(user_path, encoding='UTF-8', unrepr=True) except UnreprError as err: # DEBUG errored file # print("DEBUG: Settings migrator ERROR -------- %s path %s" % # (self._old_settings.__repr__(), user_path)) @@ -340,7 +341,7 @@ class Settings(_Section): def __init__(self, user_path): try: - _Section.__init__(self, ConfigObj(user_path, unrepr=True)) + _Section.__init__(self, ConfigObj(user_path, encoding='UTF-8', unrepr=True)) except UnreprError as error: raise ConfigurationError(error) self.excludes = Excludes(SETTINGS_DIRECTORY) diff --git a/tasks.py b/tasks.py index 7cc63fa57..8499d64fa 100644 --- a/tasks.py +++ b/tasks.py @@ -370,6 +370,8 @@ def tags_test(ctx): _set_development_path() try: import subprocess + g = subprocess.Popen(["git", "submodule", "update"]) + g.communicate(b'') p = subprocess.Popen(["/usr/bin/python", "/home/helio/github/RIDE/src/robotide/editor/tags.py"]) p.communicate(b'') finally: @@ -405,6 +407,8 @@ def test_ci(ctx, test_filter=''): try: import subprocess + g = subprocess.Popen(["git", "submodule", "update"]) + g.communicate(b'') a = subprocess.Popen(["coverage", "run", "-a", "--data-file=.coverage.1", "-m", "pytest", "--cov-config=.coveragerc", "-k test_", "-v", "utest/application/test_app_main.py"]) a.communicate(b'') From 468ca4f6fc86eedbab294ffc59eb7628637a63f2 Mon Sep 17 00:00:00 2001 From: HelioGuilherme66 Date: Sun, 27 Oct 2024 14:02:25 +0000 Subject: [PATCH 07/23] Add submodule init --- .github/workflows/linux.yml | 1 + .github/workflows/sonar.yml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index a0b98937b..85c4d1a06 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -79,6 +79,7 @@ jobs: sudo dnf install -y sdl12-compat python3-wxpython4 xorg-x11-server-Xvfb python3-pip psmisc sudo dnf downgrade -y mesa* --refresh sudo -H pip install -r requirements-dev.txt + git submodule init git submodule update - name: Run tests run: | diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index ce0f8e79a..5968375e6 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -28,9 +28,10 @@ jobs: - name: Install invoke and any other packages run: pip install coverage invoke pytest - name: Install wxPython - run: pip install https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-22.04/wxPython-4.2.0-cp310-cp310-linux_x86_64.whl + run: pip install https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-22.04/wxPython-4.2.1-cp310-cp310-linux_x86_64.whl - name: Install RIDE dependencies run: pip install -r requirements-dev.txt + git submodule init git submodule update - name: Run Xvfb run: Xvfb :1 -noreset & From ca355bbd7441776bde91822cd4fcdbb1752dfb32 Mon Sep 17 00:00:00 2001 From: HelioGuilherme66 Date: Sun, 27 Oct 2024 15:03:50 +0000 Subject: [PATCH 08/23] Change submodule init and remove sudo --- .github/workflows/linux.yml | 12 ++++++------ .github/workflows/sonar.yml | 5 ++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 85c4d1a06..7e102fa42 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -78,9 +78,8 @@ jobs: run: | sudo dnf install -y sdl12-compat python3-wxpython4 xorg-x11-server-Xvfb python3-pip psmisc sudo dnf downgrade -y mesa* --refresh - sudo -H pip install -r requirements-dev.txt - git submodule init - git submodule update + git submodule update --init --recursive + pip install -r requirements-dev.txt - name: Run tests run: | Xvfb & @@ -105,13 +104,14 @@ jobs: submodules: false - name: Fetch tags run: | - git fetch --prune --depth=1 --no-recurse-submodules + git fetch --prune --depth=1 --recurse-submodules + git submodule update --init --recursive - name: Setup environment run: | sudo apt update -y sudo apt install -y libsdl1.2debian libsdl2-2.0-0 libnotify4 - sudo pip install https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-22.04/wxPython-4.2.1-cp310-cp310-linux_x86_64.whl - sudo pip install -r requirements-dev.txt + pip install https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-22.04/wxPython-4.2.1-cp310-cp310-linux_x86_64.whl + pip install -r requirements-dev.txt - name: Run tests run: | Xvfb & diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 5968375e6..71ea64b8b 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -30,9 +30,8 @@ jobs: - name: Install wxPython run: pip install https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-22.04/wxPython-4.2.1-cp310-cp310-linux_x86_64.whl - name: Install RIDE dependencies - run: pip install -r requirements-dev.txt - git submodule init - git submodule update + run: git submodule update --init --recursive + pip install -r requirements-dev.txt - name: Run Xvfb run: Xvfb :1 -noreset & - name: Test Install RIDE From e5d6392ef189558fb83ef956f1302113f49f52a1 Mon Sep 17 00:00:00 2001 From: HelioGuilherme66 Date: Sun, 27 Oct 2024 15:20:33 +0000 Subject: [PATCH 09/23] Reorganize steps --- .github/workflows/linux.yml | 11 ++++++++--- .github/workflows/sonar.yml | 4 +++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 7e102fa42..603816dbd 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -66,14 +66,14 @@ jobs: image: fedora:latest options: --privileged steps: - - uses: actions/checkout@v3.3.0 - with: - submodules: false - name: Configure container environment run: | sudo dnf update -y sudo dnf install -y git git config --global --add safe.directory ${GITHUB_WORKSPACE} + - uses: actions/checkout@v3.3.0 + with: + submodules: false - name: Setup environment run: | sudo dnf install -y sdl12-compat python3-wxpython4 xorg-x11-server-Xvfb python3-pip psmisc @@ -85,9 +85,11 @@ jobs: Xvfb & export DISPLAY=:0 export GITHUB_ACTIONS=True + git submodule update --init --recursive invoke test-ci - name: Install and run run: | + git submodule update --init --recursive pip install . xvfb-run --server-args="-screen 0, 1280x720x24" -a ride.py & sleep 10 @@ -110,15 +112,18 @@ jobs: run: | sudo apt update -y sudo apt install -y libsdl1.2debian libsdl2-2.0-0 libnotify4 + git submodule update --init --recursive pip install https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-22.04/wxPython-4.2.1-cp310-cp310-linux_x86_64.whl pip install -r requirements-dev.txt - name: Run tests run: | Xvfb & export DISPLAY=:0 + git submodule update --init --recursive invoke test-ci - name: Install and run run: | + git submodule update --init --recursive pip install . xvfb-run --server-args="-screen 0, 1280x720x24" -a ride.py & sleep 10 diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 71ea64b8b..1bcbb6016 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -35,10 +35,12 @@ jobs: - name: Run Xvfb run: Xvfb :1 -noreset & - name: Test Install RIDE - run: pip install . + run: git submodule update --init --recursive + pip install . - name: Run RIDE unit-tests run: | export DISPLAY=:1 + git submodule update --init --recursive invoke test-ci - name: Analyze with SonarCloud uses: sonarsource/sonarcloud-github-action@master From 2882fc1a704c85dde8191c830ea485584ce9fa66 Mon Sep 17 00:00:00 2001 From: HelioGuilherme66 Date: Sun, 27 Oct 2024 18:26:36 +0000 Subject: [PATCH 10/23] Update docs for development version --- CHANGELOG.adoc | 7 +- MANIFEST.in | 1 + README.adoc | 10 +- setup.cfg | 4 + setup.py | 9 ++ src/robotide/application/CHANGELOG.html | 96 ++++++++------- src/robotide/application/releasenotes.py | 149 ++--------------------- utest/settings/test_settings.py | 2 +- 8 files changed, 88 insertions(+), 190 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 15c97b69f..3a3a993c7 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -7,7 +7,12 @@ The format is based on http://keepachangelog.com/en/1.0.0/[Keep a Changelog] and this project adheres to http://semver.org/spec/v2.0.0.html[Semantic Versioning]. -// == https://github.com/robotframework/RIDE[Unreleased] +== https://github.com/robotframework/RIDE[Unreleased] + +=== Changed + +- Changed the workflow for the development versions of RIDE. Now, development versions are taken from the ``develop`` branch, and the ``master`` will stay with released version. +- Changed the way ``configobj`` code is imported. Now is a submodule obtained from https://github.com/DiffSK/configobj. == https://github.com/robotframework/RIDE/blob/master/doc/releasenotes/ride-2.1.rst[2.1] - 2024-10-13 diff --git a/MANIFEST.in b/MANIFEST.in index 2b8dd2cce..6c69da1ac 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,5 +8,6 @@ recursive-include src *.css *.js *.py *.robot *.txt *.png *.gif *.ico *.cfg *.ht recursive-include doc *.rst recursive-include rtest *.py *.txt recursive-include src/robotide/postinstall/RIDE.app *.* +recursive-include src/robotide/preferences/configobj/src/configobj *.* recursive-include src/robotide/localization *.py *.adoc *.pot *.po *.mo diff --git a/README.adoc b/README.adoc index c58b4b137..62ad8a17c 100644 --- a/README.adoc +++ b/README.adoc @@ -22,9 +22,9 @@ You can use the tag *robotframework-ide* to search and ask on https://stackoverf == **Welcome to the development version of RIDE - next major release will be version 2.2** -// If you are looking for the latest released version, you can get the source code from **https://github.com/robotframework/RIDE/releases[releases]** or from branch **https://github.com/robotframework/RIDE/tree/release/2.0.8.1[release/2.0.8.1]** +If you are looking for the latest released version, you can get the source code from **https://github.com/robotframework/RIDE/releases[releases]** or from branch **https://github.com/robotframework/RIDE/tree/release/2.1[release/2.1]** -// See the https://github.com/robotframework/RIDE/blob/master/doc/releasenotes/ride-2.0.8.1.rst[release notes] for latest release version 2.0.8.1 +See the https://github.com/robotframework/RIDE/blob/master/doc/releasenotes/ride-2.1.rst[release notes] for latest release version 2.1 **Version https://github.com/robotframework/RIDE/tree/release/2.0.8.1[2.0.8.1] was the last release supporting Python 3.6 and 3.7** @@ -36,13 +36,13 @@ You can use the tag *robotframework-ide* to search and ask on https://stackoverf Currently, the unit tests are tested on Python 3.10, and 3.12 (which is the recommended version). Likewise, the current version of wxPython, is 4.2.2, but RIDE is known to work with 4.0.7 and 4.1.1 versions. -(3.6 <= python <= 3.11) Install current released version (*2.1*) with: +(3.8 <= python <= 3.12) Install current released version (*2.1*) with: `pip install -U robotframework-ride` -(3.8 <= python <= 3.12) Install current development version (**2.1**) with: +(3.8 <= python <= 3.12) Install current development version (**2.1.1**) with: -`pip install -U https://github.com/robotframework/RIDE/archive/master.zip` +`pip install -U https://github.com/robotframework/RIDE/archive/develop.zip` //(3.8 <= python <= 3.12) Install current Beta version (2.1b1) with: diff --git a/setup.cfg b/setup.cfg index a46dea045..199ddd8cf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,6 +10,10 @@ count = False max_line_length = 90 statistics = True +[options] +packages = find: +include_package_data = true + [options.entry_points] gui_scripts = ride = robotide.__main__:main diff --git a/setup.py b/setup.py index ab44a5d79..68bf86074 100644 --- a/setup.py +++ b/setup.py @@ -49,6 +49,14 @@ PACKAGE_DATA['robotide.localization'] = my_list[:] +my_list = [] +for curr_dir, _, files in os.walk('src/robotide/preferences/configobj/src/configobj'): + for item in files: + if '.' in item: + my_list.append(os.path.join(curr_dir, item).replace('\\', '/').replace('src/robotide/preferences/configobj/src/configobj/', '')) + +PACKAGE_DATA['robotide.preferences.configobj.src.configobj'] = my_list[:] + LONG_DESCRIPTION = """ Robot Framework is a generic test automation framework for acceptance level testing. RIDE is a lightweight and intuitive editor for Robot @@ -102,6 +110,7 @@ def run(self): url='https://github.com/robotframework/RIDE/', download_url='https://pypi.python.org/pypi/robotframework-ride', install_requires=REQUIREMENTS, + include_package_data=True, package_dir={'': SOURCE_DIR}, packages=find_packages(SOURCE_DIR), package_data=PACKAGE_DATA, diff --git a/src/robotide/application/CHANGELOG.html b/src/robotide/application/CHANGELOG.html index 2538141a6..e526208ef 100644 --- a/src/robotide/application/CHANGELOG.html +++ b/src/robotide/application/CHANGELOG.html @@ -1,6 +1,10 @@ Changelog

Changelog


All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog -and this project adheres to Semantic Versioning.

1. 2.1 - 2024-10-13

1.1. Added

  (2.1 - 2024-10-13)
+and this project adheres to Semantic Versioning.

1.1. Changed

  • +Changed the workflow for the development versions of RIDE. Now, development versions are taken from the ``develop`` branch, and the ``master`` will stay with released version. +
  • +Changed the way ``configobj`` code is imported. Now is a submodule obtained from https://github.com/DiffSK/configobj. +

2. 2.1 - 2024-10-13

2.1. Added

  (2.1 - 2024-10-13)
 - Added a setting for a specific Browser by editing the settings.cfg file. Add the string parameter
 ``browser`` in the section ``[Plugins][[Test Runner]]``.
   (2.1b1 - 2024-09-21)
@@ -55,7 +59,7 @@
 - Added ``FOR`` scope markers (``IN``, ``IN RANGE``, ``IN ENUMERATE``, ``IN ZIP``) to auto-complete list
 - Added support to read environment variable ``ROBOT_VERSION`` to apply some conditions.
 - Added note on Test Timeout that **timeout message** is not supported since Robot v3.0.1
-- Added the note, 'Colors will be active after next RIDE restart.' to the Preferences of Test Runner.

1.2. Changed

  (2.1 - 2024-10-13)
+- Added the note, 'Colors will be active after next RIDE restart.' to the Preferences of Test Runner.

2.2. Changed

  (2.1 - 2024-10-13)
 - Changed the order of insert and delete rows in Grid Editor rows context menu.
   (2.1b1 - 2024-09-21)
 - Allow to do auto-suggestions of keywords in Text Editor without a shortcut, if you want to enable or disable this feature you can config in `Tools -> Preferences -> Text Editor -> Enable auto suggestions`.
@@ -86,7 +90,7 @@
 - Changed alias marker on library imports to consider variable ``ROBOT_VERSION``. If version is lower than 6.0, uses ``'WITH NAME'``, otherwise will use ``'AS'``
   (2.0.3 - 2023-04-16)
 - Allow to do auto-suggestions of keywords in Grid Editor without a shortcut, if you want to enable or disable this feature you can config in `Tools-> Preferences -> Grid Editor -> Enable auto suggestions`
-- Made ``\\n`` visible when editing cells in Grid Editor (problematic in Windows)

1.3. Fixed

  (2.1 - 2024-10-13)
+- Made ``\\n`` visible when editing cells in Grid Editor (problematic in Windows)

2.3. Fixed

  (2.1 - 2024-10-13)
 - Fixed recognition of variables imported from YAML, JSON and Python files.
   (2.1b1 - 2024-09-21)
 - Fixed validation of multiple arguments with default values in Grid Editor.
@@ -134,18 +138,18 @@
 - Fixed clearing or emptying fixtures (Setups, Teardowns), now removes headers and synchronizes Text Editor
 - Fixed selection and persistance of colors in File Explorer and Project Tree panels
 - Fixed not using defined color for help and HTML content
-- Fixed missing newlines in sections separation

1.4. Removed

  (2.1a3 - 2024-07-22)
+- Fixed missing newlines in sections separation

2.4. Removed

  (2.1a3 - 2024-07-22)
 - Removed support for HTML file format (obsolete since Robot Framework 3.2)
-- Removed support for old Python versions, 3.6 nd 3.7.

2. 2.1b1 - 2024-09-21

2.1. Added

  • +- Removed support for old Python versions, 3.6 nd 3.7.

3. 2.1b1 - 2024-09-21

3.1. Added

  • Added color to Test Runner Console Log final output, report and log since RF v7.1rc1.
  • Added Korean language support for UI, experimental.
  • Added option ``caret style`` to change insert caret to block or line in Text Editor, by editing ``settings.cfg``. The color of the caret is the same as setting and will be adjusted for better contrast with the background. -

2.2. Changed

  • +

3.2. Changed

  • Allow to do auto-suggestions of keywords in Text Editor without a shortcut, if you want to enable or disable this feature you can config in Tools -> Preferences -> Text Editor -> Enable auto suggestions. -

2.3. Fixed

  • +

3.3. Fixed

  • Fixed validation of multiple arguments with default values in Grid Editor.
  • Fixed on Text Editor when Saving the selection of tests to run in Test Suites (Tree) is cleared. @@ -155,7 +159,7 @@ Fixed delete variable from Test Suite settings remaining in Project Explorer.
  • Fixed obsfuscation of Libraries and Metadata panels when expanding Settings in Grid Editor and Linux systems. -

3. 2.1a3 - 2024-07-22

3.1. Added

  • +

4. 2.1a3 - 2024-07-22

4.1. Added

  • Added support for Setup in keywords, since Robot Framework version 7.0.
  • Added support for new VAR marker, since Robot Framework version 7.0. @@ -196,7 +200,7 @@ Tooltips for the fields are always shown in English.
  • Colorization for language configured files is working in Text Editor. -

3.2. Fixed

  • +

4.2. Fixed

  • Fixed multiline variables in Variables section. In Text Editor they are separated by … continuation marker. In Grid Editor use | (pipe) to separate lines.
  • @@ -211,7 +215,7 @@ Fixed wrong continuation of long chains of keywords in Setups, Teardowns or Documentation
  • Fixed New User Keyword dialog not allowing empty Arguments field -

3.3. Changed

  • +

4.3. Changed

  • Improved release packaging of RIDE, by using entry_points in setuptools configuration.
  • Parsing of clipboard content to separate by cells in Grid Editor. NOTE: Need to Apply Changes in Text Editor to be effective. @@ -219,11 +223,11 @@ Improved selection of items from Tree in Text Editor. Now finds more items and selects whole line.
  • Changed output in plugin Run Anything (Macros) to allow Zoom In/Out, and Copy content. -

3.4. Removed

  • +

4.4. Removed

  • Removed support for HTML file format (obsolete since Robot Framework 3.2)
  • Removed support for old Python versions, 3.6 nd 3.7. -

4. 2.0.8.1 - 2023-11-01

4.1. Added

  • +

5. 2.0.8.1 - 2023-11-01

5.1. Added

  • Added auto update check when development version is installed
  • Added menu option ``Help→Check for Upgrade`` which allows to force update check and install development version @@ -242,7 +246,7 @@ Added variables creation shortcuts (``Ctrl-1,2,5``) to fields Arguments in Grid Editor
  • Added support for JSON variables, by using the installed Robot Framework import method -

4.2. Fixed

  • +

5.2. Fixed

  • Fixed escaped spaces showing in Text Editor on commented cells
  • Fixed resource files dissapearing from Project tree on Windows @@ -260,7 +264,7 @@ Position of cursor in Text Editor auto-suggestions when line contains multibyte characters
  • Drag and drop of variables defined with comments between resource files -

4.3. Changed

  • +

5.3. Changed

  • Improved keywords documentation search, by adding current dir to search
  • Improved Move up/down, ``Alt-UpArrow``/``Alt-DownArrow`` in Text Editor, to have proper indentation and selection @@ -276,14 +280,14 @@ Improved keyword ``Find Usages`` to return more matches. Fails to find mixed spaces and ``_``
  • In Grid Editor ``Ctrl-Shift-4`` now replaces escaped spaces ``\\ `` by spaces -

5. 2.0.7 - 2023-08-13

5.1. Added

  • +

6. 2.0.7 - 2023-08-13

6.1. Added

  • Added indication of matching brackets, ``()``, ``{}``, ``[]``, in Text Editor
  • Added context menu to RIDE tray icon. Options Show, Hide and Close
  • Added sincronization with Project Explorer to navigate to selected item, Test Case, Keyword, Variable, in Text Editor Note: This feature is working fine in Fedora 38, but not on Windows and macOS. -

5.2. Fixed

  • +

6.2. Fixed

  • Fixed non syncronized expanding/collapse of Settings panel in Grid Editor, on Linux
  • Fixed not working the deletion of cells commented with ``\# `` in Grid Editor with ``Ctrl-Shift-D`` @@ -297,17 +301,17 @@ Fixed title of User Keyword in Grid Editor always showing ``Find Usages`` instead of the keyword name
  • Fixed renaming keywords when they were arguments of ``Run Keywords`` in Setups and Teardowns -

5.3. Changed

  • +

6.3. Changed

  • Improve Text Editor auto-suggestions to keep libraries prefixes. -

6. 2.0.6 - 2023-06-10

6.1. Added

  • +

7. 2.0.6 - 2023-06-10

7.1. Added

  • Added boolean parameter ``filter newlines`` to Grid Editor with default ``True``, to hide or show newlines in cells -

6.2. Changed

  • +

7.2. Changed

  • Changed ``tasks.py`` to test ``utest/application/test_app_main.py`` isolated from the other tests
  • Improve auto-suggestions of keywords in Grid Editor by allowing to close suggestions list with keys ARROW_LEFT or ARROW_RIGHT
  • Improve Text Editor auto-suggestions by using: selected text, text at left or at right of cursor -

7. 2.0.5 - 2023-05-08

7.1. Added

  • +

8. 2.0.5 - 2023-05-08

8.1. Added

  • Added ``FOR`` scope markers (``IN``, ``IN RANGE``, ``IN ENUMERATE``, ``IN ZIP``) to auto-complete list
  • Added support to read environment variable ``ROBOT_VERSION`` to apply some conditions. @@ -315,15 +319,15 @@ Added note on Test Timeout that timeout message is not supported since Robot v3.0.1
  • Added the note, Colors will be active after next RIDE restart. to the Preferences of Test Runner. -

7.2. Changed

  • +

8.2. Changed

  • Changed alias marker on library imports to consider variable ``ROBOT_VERSION``. If version is lower than 6.0, uses ``WITH NAME``, otherwise will use ``AS`` -

8. Fixed

  • +

9. Fixed

  • Fixed auto-indent on block commands in Text Editor -

9. 2.0.3 - 2023-04-16

9.1. Changed

  • +

10. 2.0.3 - 2023-04-16

10.1. Changed

  • Allow to do auto-suggestions of keywords in Grid Editor without a shortcut, if you want to enable or disable this feature you can config in Tools-> Preferences -> Grid Editor -> Enable auto suggestions
  • Made ``\\n`` visible when editing cells in Grid Editor (problematic in Windows) -

10. Fixed

  • +

11. Fixed

  • Fixed missing auto-enclosing when in Cell Editor in Linux
  • Fixed RIDE will crash when using third party input method in Mac OS @@ -337,7 +341,7 @@ Fixed not using defined color for help and HTML content
  • Fixed missing newlines in sections separation -

11. 2.0 - 2023-03-01

11.1. Added

  (2.0rc1 - 2023-02-26)
+

12. 2.0 - 2023-03-01

12.1. Added

  (2.0rc1 - 2023-02-26)
 - Minimal support to accept `*** Comments ***` sections (unfinished code)
 - Added insert and delete cells to Text Editor, by using ``Ctrl-Shift-I`` and ``Ctrl-Shift-D``
 - Added move up and move down rows to Text Editor, by using ``Alt-Up`` and ``Alt-Down``
@@ -390,13 +394,13 @@
 - Added enclosing text in Text Editor or selected text with certain symbols
 - Added enclosing text in Grid Editor or selected text with certain symbols
 - Added 8s timer to shortcut creation dialog on install
-- Added process memory limit on Messages Log

11.2. Removed

  (2.0b2 - 2022-09-05)
+- Added process memory limit on Messages Log

12.2. Removed

  (2.0b2 - 2022-09-05)
 - Removed ``robotframeworklexer`` dependency and local copy
 - Removed alignment flag on grid cell JSON Editor (Ctrl-Shift-J)
 - Removed moving to keyword/variable definition when doing Double-Click in grid cell
   (2.0b1 - 2020-07-26)
 - Python 2.7 support
-- wxPython/wxPhoenix version conditioning

11.3. Changed

  (2.0b3 - 2023-01-15)
+- wxPython/wxPhoenix version conditioning

12.3. Changed

  (2.0b3 - 2023-01-15)
 - Hiding items in Test Suites explorer with names starting with #
 - Disabled the Close button on the Test Suites explorer
   This was causing not being possible to restore it, unless editing the settings.cfg file.
@@ -425,7 +429,7 @@
 - Changed icon background to white
 - Made Project Tree and File Explorer panels, Plugins.
 - wx.NewId() to wx.NewIdRef()
-- Separated AppendText for Messages Log

11.4. Fixed

  (2.0rc1 - 2023-02-26)
+- Separated AppendText for Messages Log

12.4. Fixed

  (2.0rc1 - 2023-02-26)
 - Fixed blank Grid Editor at keywords with steps commented with ``\# ``, by using ``Ctrl-Shift-3 on Text Editor
   (2.0b3 - 2023-01-15)
 . Fixed low performance when opening large projects
@@ -499,7 +503,7 @@
 - Fixed Settings editor
 - Fixed blank Edit screen
 - Fixed Runner arguments parsing
-- Fixed Runner Log window Chinese and Latin encoding chars on Windows

12. 2.0rc1 - 2023-02-26

12.1. Added

  • +- Fixed Runner Log window Chinese and Latin encoding chars on Windows

13. 2.0rc1 - 2023-02-26

13.1. Added

  • Minimal support to accept *** Comments *** sections (unfinished code)
  • Added insert and delete cells to Text Editor, by using ``Ctrl-Shift-I`` and ``Ctrl-Shift-D`` @@ -507,30 +511,30 @@ Added move up and move down rows to Text Editor, by using ``Alt-Up`` and ``Alt-Down``
  • Added insert and delete rows to Text Editor, by using ``Ctrl-I`` and ``Ctrl-D`` -

12.2. Removed

12.3. Changed

12.4. Fixed

  • +

13.2. Removed

13.3. Changed

13.4. Fixed

  • Fixed blank Grid Editor at keywords with steps commented with ``\# ``, by using ``Ctrl-Shift-3 on Text Editor -

13. 2.0b3 - 2023-01-15

13.1. Added

  • +

14. 2.0b3 - 2023-01-15

14.1. Added

  • Added swap row up, by using ``Ctrl-T``
  • Added commenting/uncommenting of content with ``\# ``, by using ``Ctrl-Shift-3`` and ``Ctrl-Shift-4``
  • Added support for editing .robot and .resource files with content before sections -

13.2. Removed

  • +

14.2. Removed

  • None -

13.3. Changed

  • +

14.3. Changed

  • Hiding items in Test Suites explorer with names starting with #
  • Disabled the Close button on the Test Suites explorer This was causing not being possible to restore it, unless editing the settings.cfg file. Other reason was to prevent user to closing it, after detaching the panel, and re-attaching, which has a bug making the Tree not visible. -

13.4. Fixed

  1. +

14.4. Fixed

  1. Fixed low performance when opening large projects

    • Fixed comment and uncomment in Grid Editor when cells contain more than one variables assignement
    • Fixed console log stopping to output certain characters, like chinese and latin -

14. 2.0b2 - 2022-09-05

14.1. Added

  • +

15. 2.0b2 - 2022-09-05

15.1. Added

  • Added menu entry at Help → Offline Change Log to view this file on disk
  • Added skipped tests counter and corresponding colored icon on Project tree @@ -598,13 +602,13 @@ When editing, Ctrl-Home and Ctrl-End move cursor to start and end of cell content respectively.
  • Added Del key to clear Grid Editor cell content when in navigation mode (clear like doing Ctrl-X) -
  • 14.2. Removed

    • +

    15.2. Removed

    • Removed ``robotframeworklexer`` dependency and local copy
    • Removed alignment flag on grid cell JSON Editor (Ctrl-Shift-J)
    • Removed moving to keyword/variable definition when doing Double-Click in grid cell -

    14.3. Changed

    • +

    15.3. Changed

    • Unit tests to use ``pytest`` and removed ``nose`` dependency. Support for Python 3.10 at unit test level.
    • Prevent expanding Tests and change selection on Project tree (when right-clicking) @@ -634,7 +638,7 @@ Changed Enter button in navigation mode to start editing cell, and to move to right cell when in edit mode
    • Performance improvements for loading large test suites -

    14.4. Fixed

    • +

    15.4. Fixed

    • Fixed missing menu icons on Linux (was working on Windows)
    • Fixed removal of animation in Project tree when test run is interrupted @@ -707,7 +711,7 @@ Fixed RIDE startup crash when Tree or File Explorer plugins use opened=False setting
    • Fixed error occurring when deleting test cases on Tree -

    15. 2.0b1 - 2020-07-26

    15.1. Added

    • +

    16. 2.0b1 - 2020-07-26

    16.1. Added

    • Added CHANGELOG.adoc
    • Added ignoring log.html and report.html on reporting HTML test suites @@ -725,11 +729,11 @@ Added 8s timer to shortcut creation dialog on install
    • Added process memory limit on Messages Log -

    15.2. Removed

    • +

    16.2. Removed

    • Python 2.7 support
    • wxPython/wxPhoenix version conditioning -

    15.3. Changed

    • +

    16.3. Changed

    • Improved filesystem changes detection, with a confirmation dialog to reload workspace
    • Changed dependency on wx.Window on tree panel @@ -743,7 +747,7 @@ wx.NewId() to wx.NewIdRef()
    • Separated AppendText for Messages Log -

    15.4. Fixed

    • +

    16.4. Fixed

    • Fixed editing cells in Grid Editor on wxPython 4.1
    • Fixed not saving file after deleting text in Text Editor @@ -803,12 +807,12 @@ Fixed Runner arguments parsing
    • Fixed Runner Log window Chinese and Latin encoding chars on Windows -

    16. 1.7.4.2 - 2020-01-20

    16.1. Added

    • +

    17. 1.7.4.2 - 2020-01-20

    17.1. Added

    • wxPython version locked up to 4.0.7.post2. -

    16.2. Removed

    • +

    17.2. Removed

    • None -

    16.3. Changed

    • +

    17.3. Changed

    • None -

    16.4. Fixed

    • +

    17.4. Fixed

    • None
    diff --git a/src/robotide/application/releasenotes.py b/src/robotide/application/releasenotes.py index 3c1f061e6..63a391bfe 100644 --- a/src/robotide/application/releasenotes.py +++ b/src/robotide/application/releasenotes.py @@ -125,7 +125,7 @@ def set_content(self, html_win, content): milestone = re.split('[ab-]', VERSION)[0] WELCOME_TEXT = f""" -

    Welcome to use RIDE version {version}

    +

    Welcome to use RIDE DEVELOPMENT version {version}

    Thank you for using the Robot Framework IDE (RIDE).

    @@ -148,52 +148,17 @@ def set_content(self, html_win, content):
    -

    RIDE is celebrating 16 years on this date!

    -

    RIDE (Robot Framework IDE) {VERSION} is a new release with important enhancements and bug fixes. The reference for valid arguments is Robot Framework installed version, which is at this - moment 7.1. However, internal library code is originally based on version 3.1.2, but adapted for new versions.

    + moment 7.1.1. However, internal library code is originally based on version 3.1.2, but adapted for new versions.

    • This version supports Python 3.8 up to 3.12.
    • There are some changes, or known issues:
        -
      • ❌ - Removed support for Python 3.6 and 3.7
      • -
      • ✔ - Fixed recognition of variables imported from YAML, JSON and Python files.
      • -
      • ✔ - Added a setting for a specific Browser by editing the settings.cfg file. Add the string parameter -browser in the section [Plugins][[Test Runner]]
      • -
      • ✔ - Fixed on Text Editor when Saving the selection of tests to run in Test Suites (Tree) is cleared.
      • -
      • ✔ - Added Korean language support for UI.
      • -
      • ✔ - Added caret style to change insert caret to 'block' or 'line' in Text Editor, by editing -settings.cfg. The color of the caret is the same as 'setting' and will be adjusted for better contrast with the - background.
      • -
      • ✔ - Allow to do auto-suggestions of keywords in Text Editor without a shortcut, if you want to enable or disable -this feature you can config in `Tools -> Preferences -> Text Editor -> Enable auto suggestions`.
      • -
      • ✔ - Added support for Setup in keywords, since Robot Framework version 7.0.
      • -
      • ✔ - Added support for new VAR marker, since Robot Framework version 7.0.
      • -
      • ✔ - Added to Grid Editor changing Zoom In/Out with Ctrl-Mouse Wheel and setting at Preferences.
      • -
      • ✔ - Fixed plugin Run Anything (Macros) not showing output and broken actions.
      • -
      • ✔ - Added actions on columns of Grid Editor: Double-Click or Right Mouse Click, allows to edit the column name for - Data -Driven or Templated; Left Mouse Click, selects the column cells.
      • -
      • ✔ - Added command line option, --settingspath, to select a different configuration.
      • -
      • ✔ - Added different settings file, according the actual Python executable, if not the original installed.
      • -
      • ✔ - Added a selector for Tasks and Language to the New Project dialog.
      • -
      • ✔ - Added UI localization prepared for all the languages from installed Robot Framework version 6.1, or -higher. Major translations are: Dutch, Portuguese and Brazilian Portuguese. Still missing translation -of some elements.
      • -
      • ✔ - Added support for language configured test suites, with languages from installed Robot Framework version 6.1, - or -higher.
      • -
      • ✔ - On Text Editor, pressing Ctrl when the caret/cursor is near a Keyword will show a detachable window with - the -documentation, at Mouse Pointer position.
      • -
      • ✔ - RIDE tray icon now shows a context menu with options Show, Hide and Close.
      • -
      • ✔ - Highlighting and navigation of selected Project Explorer items, in Text Editor.
      • -
      • ✔ - When editing in Grid Editor with content assistance, the selected content can be edited by escaping the list of -suggestions with keys ARROW_LEFT or ARROW_RIGHT.
      • -
      • ✔ - Newlines in Grid Editor can be made visible with the filter newlines set to False.
      • +
      • 🐞 - When upgrading RIDE and activate Restart, some errors are visible about missing /language file, and behaviour + is not normal. Better to close RIDE and start a new instance.
      • 🐞 - Problems with COPY/PASTE in Text Editor have been reported when using wxPython 4.2.0, but not with version 4.2.1 and 4.2.2, which we now recommend.
      • 🐞 - Some argument types detection (and colorization) is not correct in Grid Editor.
      • @@ -204,94 +169,7 @@ def set_content(self, html_win, content):

      New Features and Fixes Highlights

        -
      • Fixed recognition of variables imported from YAML, JSON and Python files.
      • -
      • Added a setting for a specific Browser by editing the settings.cfg file. Add the string parameter -browser in the section [Plugins][[Test Runner]]
      • -
      • Changed the order of insert and delete rows in Grid Editor rows context menu.
      • -
      • Fixed validation of multiple arguments with default values in Grid Editor.
      • -
      • Added color to Test Runner Console Log final output, report and log since RF v7.1rc1.
      • -
      • Fixed on Text Editor when Saving the selection of tests to run in Test Suites (Tree) is cleared.
      • -
      • Added Korean language support for UI, experimental.
      • -
      • Fixed wrong item selection, like Test Suite, when doing right-click actions in Project Explorer.
      • -
      • Fixed delete variable from Test Suite settings remaining in Project Explorer.
      • -
      • Added caret style to change insert caret to 'block' or 'line' in Text Editor, by editing -settings.cfg. The color of the caret is the same as 'setting' and will be adjusted for better contrast with the - background.
      • -
      • Fixed obsfuscation of Libraries and Metadata panels when expanding Settings in Grid Editor and Linux systems.
      • -
      • Allow to do auto-suggestions of keywords in Text Editor without a shortcut, if you want to enable or disable -this feature you can config in `Tools -> Preferences -> Text Editor -> Enable auto suggestions`.
      • -
      • Added support for Setup in keywords, since Robot Framework version 7.0.
      • -
      • Fixed multiline variables in Variables section. In Text Editor they are separated by ... continuation marker. -In Grid Editor use | (pipe) to separate lines.
      • -
      • Added support for new VAR marker, since Robot Framework version 7.0.
      • -
      • Added configurable style of the tabs in notebook pages, Edit, Text, Run, etc. Parameter notebook theme - takes values from 0 to 5. See wxPython, demo for agw.aui for details.
      • -
      • Added UI localization and support for Japanese configured test suites, valid for Robot Framework version 7.0.1 or - higher.
      • -
      • Fixed keywords Find Usages in Grid Editor not finding certain values when using Gherkin.
      • -
      • Improved selection of items from Tree in Text Editor. Now finds more items and selects whole line.
      • -
      • Changed output in plugin Run Anything (Macros) to allow Zoom In/Out, and Copy content.
      • -
      • Added to Grid Editor changing Zoom In/Out with Ctrl-Mouse Wheel and setting at Preferences.
      • -
      • Fixed plugin Run Anything (Macros) not showing output and broken actions.
      • -
      • Added actions on columns of Grid Editor: Double-Click or Right Mouse Click, allows to edit the column name for Data -Driven or Templated; Left Mouse Click, selects the column cells.
      • -
      • Added command line option, --settingspath, to select a different configuration.
      • -
      • Added different settings file, according the actual Python executable, if not the original installed.
      • -
      • Fixed headers and blank spacing in Templated tests.
      • -
      • Added context option Open Containing Folder to test suites directories in Project Explorer.
      • -
      • Added a setting for a specific file manager by editing the settings.cfg file. Add the string parameter -file manager -in the section [General]
      • -
      • Added minimal support to have comment lines in Import settings. These are not supposed to be edited in Editor, -and new lines are added at Text Editor.
      • -
      • Fixed removal of continuation marker in steps
      • -
      • Fixed wrong continuation of long chains of keywords in Setups, Teardowns or Documentation.
      • -
      • Added a selector for Tasks and Language to the New Project dialog. Still some problems: Tasks type changes to Tests, -localized sections only stay translated after Apply in Text Editor.
      • -
      • Added UI localization prepared for all the languages from installed Robot Framework version 6.1, or -higher. Language is selected from Tools->Preferences->General.
      • -
      • Removed support for HTML file format (obsolete since Robot Framework 3.2)
      • -
      • Added support for language configured test suites. Fields are shown in the language of the files in Grid Editor. - Tooltips are always shown in English. Colorization for language configured files is working in Text Editor.
      • -
      • Fixed New User Keyword dialog not allowing empty Arguments field
      • -
      • Fixed escaped spaces showing in Text Editor on commented cells
      • -
      • Improved keywords documentation search, by adding current dir to search
      • -
      • Improved Move up/down, Alt-UpArrow/Alt-DownArrow in Text Editor, to have proper indentation and -selection
      • -
      • Added auto update check when development version is installed
      • -
      • Added menu option Help->Check for Upgrade which allows to force update check and install development -version
      • -
      • Added Upgrade Now action to update dialog.
      • -
      • Added Test Tags field (new, since Robot Framework 6.0) to Test Suites settings. This field will replace -Default and Force Tags settings, after Robot Framework 7.0
      • -
      • Improved RIDE Log and Parser Log windows to allow Zoom In/Out with Ctrl-Mouse Wheel
      • -
      • Hide continuation markers in Project Tree
      • -
      • Improved content assistance in Text Editor by allowing to filter list as we type
      • -
      • Fixed resource files disappearing from Project tree on Windows
      • -
      • Fixed missing indication of link for User Keyword, when pressing Ctrl in Grid Editor
      • -
      • Added content help pop-up on Text Editor by pressing Ctrl for text at cursor position or selected -autocomplete list item
      • -
      • Added Exclude option in context nenu for Test files, previously was only possible for Test Suites folders
      • -
      • Added exclusion of monitoring filesystem changes for files and directories excluded in Preferences
      • -
      • Fixed exception when finding GREY color for excluded files and directories in Project Tree
      • -
      • Added support for JSON variables, by using the installed Robot Framework import method
      • -
      • Colorization of Grid Editor cells after the continuation marker ... and correct parsing of those lines
      • -
      • Colorization of Grid Editor cells when contents is list or dictionary variables
      • -
      • Added indication of matching brackets, (), """'''{}'''f""", [], in Text Editor
      • -
      • Fixed non synchronized expanding/collapse of Settings panel in Grid Editor, on Linux
      • -
      • Fixed not working the deletion of cells commented with # in Grid Editor with Ctrl-Shift-D
      • -
      • Fixed empty line being always added to the Variables section in Text Editor
      • -
      • Improved project file system changes and reloading
      • -
      • Added context menu to RIDE tray icon. Options Show, Hide and Close
      • -
      • Added synchronization with Project Explorer to navigate to selected item, Test Case, Keyword, Variable, in Text - Editor
      • -
      • Control commands (FOR, IF, TRY, etc) will only be colorized as valid keywords when typed in -all caps in Grid Editor
      • -
      • Newlines in Grid Editor can be made visible with the filter newlines set to False, by editing -settings.cfg
      • -
      • Improve auto-suggestions of keywords in Grid Editor by allowing to close suggestions list with keys ARROW_LEFT or -ARROW_RIGHT
      • -
      • Improve Text Editor auto-suggestions by using: selected text, text at left or at right of cursor
      • +
      • Fixed double action on Linux when pressing the DEL key
      @@ -313,11 +191,11 @@ def set_content(self, html_win, content): issue tracker. You should see Robot Framework Forum if your problem is already known.

      -

      To install with pip installed, just run

      +

      To install the latest release with pip installed, just run

      -pip install --upgrade robotframework-ride=={VERSION}
      +pip install --upgrade robotframework-ride==2.1
       
      -

      to install exactly this release, which is the same as using

      +

      to install exactly the specified release, which is the same as using

       pip install --upgrade robotframework-ride
       
      @@ -330,26 +208,23 @@ def set_content(self, html_win, content): If you want to help in the development of RIDE, by reporting issues in current development version, you can install with:

      -pip install -U https://github.com/robotframework/RIDE/archive/master.zip
      +pip install -U https://github.com/robotframework/RIDE/archive/develop.zip
       

      Important document for helping with development is the CONTRIBUTING.adoc.

      -

      See the FAQ for - important info about : FOR changes and other known issues and workarounds.

      + href="https://github.com/robotframework/RIDE/blob/develop/CONTRIBUTING.adoc">CONTRIBUTING.adoc.

      To start RIDE from a command window, shell or terminal, just enter:

      ride

      You can also pass some arguments, like a path for a test suite file or directory.

      ride example.robot

      Another possible way to start RIDE is:

      -python -m robotide.__init__
      +python -m robotide
       

      You can then go to Tools>Create RIDE Desktop Shortcut, or run the shortcut creation script with:

      python -m robotide.postinstall -install

      or

      ride_postinstall.py -install
      -

      RIDE {VERSION} was released on 13/October/2024 (16 years after its first version).

      +

      RIDE {VERSION} was released on 27/October/2024.

      @@ -313,11 +191,11 @@ def set_content(self, html_win, content): issue tracker. You should see Robot Framework Forum if your problem is already known.

      -

      To install with pip installed, just run

      +

      To install the latest release with pip installed, just run

      -pip install --upgrade robotframework-ride=={VERSION}
      +pip install --upgrade robotframework-ride==2.1
       
      -

      to install exactly this release, which is the same as using

      +

      to install exactly the specified release, which is the same as using

       pip install --upgrade robotframework-ride
       
      @@ -330,26 +208,23 @@ def set_content(self, html_win, content): If you want to help in the development of RIDE, by reporting issues in current development version, you can install with:

      -pip install -U https://github.com/robotframework/RIDE/archive/master.zip
      +pip install -U https://github.com/robotframework/RIDE/archive/develop.zip
       

      Important document for helping with development is the CONTRIBUTING.adoc.

      -

      See the FAQ for - important info about : FOR changes and other known issues and workarounds.

      + href="https://github.com/robotframework/RIDE/blob/develop/CONTRIBUTING.adoc">CONTRIBUTING.adoc.

      To start RIDE from a command window, shell or terminal, just enter:

      ride

      You can also pass some arguments, like a path for a test suite file or directory.

      ride example.robot

      Another possible way to start RIDE is:

      -python -m robotide.__init__
      +python -m robotide
       

      You can then go to Tools>Create RIDE Desktop Shortcut, or run the shortcut creation script with:

      python -m robotide.postinstall -install

      or

      ride_postinstall.py -install
      -

      RIDE {VERSION} was released on 13/October/2024 (16 years after its first version).

      +

      RIDE {VERSION} was released on 27/October/2024.