diff --git a/docs/requirements.txt b/docs/requirements.txt index 048a95b88bf..aa96fc5a2bd 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ +livereload==2.6.2;python_version>="3.6" mkdocs==1.1.2 mkdocs-material==5.2.3 mkdocs-simple-hooks==0.1.1 diff --git a/examples/my_first_test.py b/examples/my_first_test.py index f839c4d0b67..97b188f01f6 100755 --- a/examples/my_first_test.py +++ b/examples/my_first_test.py @@ -57,7 +57,7 @@ def test_basic(self): # * Types in the new text # * Hits Enter/Submit (if the text ends in "\n") # - # self.update_text(S, T) can also be written as self.type(S, T) + # self.update_text(S, T) can also be written as self.input(S, T) # # 4. There's usually more than one way to do the same thing. Ex: # [ @@ -95,6 +95,10 @@ def test_basic(self): # the element does not appear on the page within the timeout limit. # And self.assert_element() does this too (without returning it). # - # 7. For the full method list, see one of the following: + # 7. If a URL starts with "://", then "https://" is automatically used. + # Example: [self.open("://URL")] becomes [self.open("https://URL")] + # This helps by reducing the line length by 5 characters. + # + # 8. For the full method list, see one of the following: # * SeleniumBase/seleniumbase/fixtures/base_case.py # * SeleniumBase/help_docs/method_summary.md diff --git a/examples/translations/ReadMe.md b/examples/translations/ReadMe.md index 4168f35b9eb..89b825d195b 100755 --- a/examples/translations/ReadMe.md +++ b/examples/translations/ReadMe.md @@ -51,6 +51,9 @@ seleniumbase translate [SB_FILE].py [LANGUAGE] [ACTION] ``-o`` / ``--overwrite`` (Overwrite the file being translated) ``-c`` / ``--copy`` (Copy the translation to a new ``.py`` file) +* Options: +``-n`` (include line Numbers when using the Print action) + * Examples: Translate test_1.py into Chinese and only print the output: >>> seleniumbase translate test_1.py --zh -p diff --git a/requirements.txt b/requirements.txt index 725345221ad..cae97abfad7 100755 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ requests==2.23.0 selenium==3.141.0 pluggy==0.13.1 attrs>=19.3.0 -pytest==4.6.10;python_version<"3.5" +pytest==4.6.11;python_version<"3.5" pytest==5.4.3;python_version>="3.5" pytest-cov==2.9.0 pytest-forked==1.1.3 @@ -41,11 +41,11 @@ coverage==5.1 pyotp==2.3.0 boto==2.49.0 cffi==1.14.0 -rich==1.3.1;python_version>="3.6" and python_version<"4.0" +rich==2.0.0;python_version>="3.6" and python_version<"4.0" flake8==3.7.9;python_version<"3.5" flake8==3.8.2;python_version>="3.5" pyflakes==2.1.1;python_version<"3.5" pyflakes==2.2.0;python_version>="3.5" -certifi>=2020.4.5.1 +certifi>=2020.4.5.2 pdfminer.six==20191110;python_version<"3.5" pdfminer.six==20200517;python_version>="3.5" diff --git a/seleniumbase/console_scripts/ReadMe.md b/seleniumbase/console_scripts/ReadMe.md index fdc274d5539..14e921067ca 100755 --- a/seleniumbase/console_scripts/ReadMe.md +++ b/seleniumbase/console_scripts/ReadMe.md @@ -71,6 +71,9 @@ See: http://www.katalon.com/automation-recorder ``-o`` / ``--overwrite`` (Overwrite the file being translated) ``-c`` / ``--copy`` (Copy the translation to a new ``.py`` file) +* Options: +``-n`` (include line Numbers when using the Print action) + * Output: Translates a SeleniumBase Python file into the language specified. Method calls and "import" lines get swapped. diff --git a/seleniumbase/console_scripts/run.py b/seleniumbase/console_scripts/run.py index 75ee194652f..698f11abd3f 100644 --- a/seleniumbase/console_scripts/run.py +++ b/seleniumbase/console_scripts/run.py @@ -9,6 +9,7 @@ sbase install chromedriver sbase mkdir browser_tests sbase convert my_old_webdriver_unittest.py +sbase print my_first_test.py -n sbase translate my_first_test.py --zh -p sbase extract-objects my_first_test.py sbase inject-objects my_first_test.py @@ -67,6 +68,7 @@ def show_basic_usage(): sc += (" install [DRIVER_NAME] [OPTIONS]\n") sc += (" mkdir [NEW_TEST_DIRECTORY_NAME]\n") sc += (" convert [PYTHON_WEBDRIVER_UNITTEST_FILE]\n") + sc += (" print [FILE] [OPTIONS]\n") sc += (" translate [SB_PYTHON_FILE] [LANGUAGE] [ACTION]\n") sc += (" extract-objects [SB_PYTHON_FILE]\n") sc += (" inject-objects [SB_PYTHON_FILE] [OPTIONS]\n") @@ -149,6 +151,20 @@ def show_convert_usage(): print("") +def show_print_usage(): + print(" ** print **") + print(" Usage:") + print(" seleniumbase print [FILE] [OPTIONS]") + print(" OR: sbase print [FILE] [OPTIONS]") + print(" Options:") + print(" -n (Add line Numbers to the rows)") + print(" -w (Use word-Wrap for long lines)") + print(" Output:") + print(" Prints the code/text of any file") + print(" with syntax-highlighting.") + print("") + + def show_translate_usage(): print(" ** translate **") print(" Usage:") @@ -164,6 +180,8 @@ def show_translate_usage(): print(" -p / --print (Print translation output to the screen)") print(" -o / --overwrite (Overwrite the file being translated)") print(" -c / --copy (Copy the translation to a new .py file)") + print(" Options:") + print(" -n (include line Numbers when using the Print action)") print(" Output:") print(" Translates a SeleniumBase Python file into the language") print(' specified. Method calls and "import" lines get swapped.') @@ -342,6 +360,7 @@ def show_detailed_help(): show_install_usage() show_mkdir_usage() show_convert_usage() + show_print_usage() show_translate_usage() show_extract_objects_usage() show_inject_objects_usage() @@ -390,6 +409,22 @@ def main(): else: show_basic_usage() show_convert_usage() + elif command == "print": + if len(command_args) >= 1: + if sys.version_info[0] == 2: + colorama.init(autoreset=True) + c5 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + msg = '"sbase print" does NOT support Python 2! ' + msg += 'Try using the Unix "cat" command instead!' + message = "\n" + c5 + msg + cr + "\n" + print("") + raise Exception(message) + from seleniumbase.console_scripts import sb_print + sb_print.main() + else: + show_basic_usage() + show_print_usage() elif command == "translate": if len(command_args) >= 1: if sys.version_info[0] == 2: @@ -478,6 +513,10 @@ def main(): print("") show_convert_usage() return + elif command_args[0] == "print": + print("") + show_print_usage() + return elif command_args[0] == "translate": print("") show_translate_usage() diff --git a/seleniumbase/console_scripts/sb_print.py b/seleniumbase/console_scripts/sb_print.py new file mode 100755 index 00000000000..a9fb85771b6 --- /dev/null +++ b/seleniumbase/console_scripts/sb_print.py @@ -0,0 +1,342 @@ +""" +Prints the code/text of any file with syntax-highlighting + +Usage: + seleniumbase print [FILE] [OPTIONS] + OR: sbase print [FILE] [OPTIONS] +Options: + -n (Add line Numbers to the rows) + -w (Use word-Wrap for long lines) +Output: + Prints the code/text of any file + with syntax-highlighting. +""" + +import colorama +import os +import sys + + +def invalid_run_command(msg=None): + exp = (" ** print **\n\n") + exp += " Usage:\n" + exp += " seleniumbase print [FILE] [OPTIONS]\n" + exp += " OR: sbase print [FILE] [OPTIONS]\n" + exp += " Options:\n" + exp += " -n (Add line Numbers to the rows)\n" + exp += " -w (Use word-Wrap for long lines)\n" + exp += " Output:\n" + exp += " Prints the code/text of any file\n" + exp += ' with syntax-highlighting.\n' + if not msg: + raise Exception('INVALID RUN COMMAND!\n\n%s' % exp) + else: + raise Exception('INVALID RUN COMMAND!\n%s\n\n%s' % (msg, exp)) + + +def sc_ranges(): + # Get the ranges of special characters of Chinese, Japanese, and Korean. + special_char_ranges = ([ + {"from": ord(u"\u3300"), "to": ord(u"\u33ff")}, + {"from": ord(u"\ufe30"), "to": ord(u"\ufe4f")}, + {"from": ord(u"\uf900"), "to": ord(u"\ufaff")}, + {"from": ord(u"\U0002F800"), "to": ord(u"\U0002fa1f")}, + {'from': ord(u'\u3040'), 'to': ord(u'\u309f')}, + {"from": ord(u"\u30a0"), "to": ord(u"\u30ff")}, + {"from": ord(u"\u2e80"), "to": ord(u"\u2eff")}, + {"from": ord(u"\u4e00"), "to": ord(u"\u9fff")}, + {"from": ord(u"\u3400"), "to": ord(u"\u4dbf")}, + {"from": ord(u"\U00020000"), "to": ord(u"\U0002a6df")}, + {"from": ord(u"\U0002a700"), "to": ord(u"\U0002b73f")}, + {"from": ord(u"\U0002b740"), "to": ord(u"\U0002b81f")}, + {"from": ord(u"\U0002b820"), "to": ord(u"\U0002ceaf")} + ]) + return special_char_ranges + + +def is_cjk(char): + # Returns True if the special character is Chinese, Japanese, or Korean. + sc = any( + [range["from"] <= ord(char) <= range["to"] for range in sc_ranges()]) + return sc + + +def get_width(line): + # Return the true width of the line. Not the same as line length. + # Chinese/Japanese/Korean characters take up double width visually. + line_length = len(line) + for char in line: + if is_cjk(char): + line_length += 1 + return line_length + + +def main(): + colorama.init(autoreset=True) + c5 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c7 = colorama.Fore.BLACK + colorama.Back.MAGENTA + cr = colorama.Style.RESET_ALL + line_numbers = False + word_wrap = False + help_me = False + invalid_cmd = None + is_python_file = False + code_lang = None + + command_args = sys.argv[2:] + file_to_print = command_args[0] + if file_to_print.lower().endswith('.py'): + is_python_file = True + code_lang = "python" + elif file_to_print.lower().endswith('.js'): + code_lang = "javascript" + elif file_to_print.lower().endswith('.md'): + code_lang = "markdown" + elif file_to_print.lower().endswith('.html'): + code_lang = "html" + elif file_to_print.lower().endswith('.css'): + code_lang = "css" + elif file_to_print.lower().endswith('.go'): + code_lang = "go" + elif file_to_print.lower().endswith('.java'): + code_lang = "java" + elif "." not in file_to_print: + code_lang = "markdown" + else: + code_lang = file_to_print.split(".")[-1].lower() + + if len(command_args) >= 2: + options = command_args[1:] + for option in options: + option = option.lower() + if option == "-n": + line_numbers = True + elif option == "-w": + word_wrap = True + else: + invalid_cmd = "\n===> INVALID OPTION: >> %s <<\n" % option + invalid_cmd = invalid_cmd.replace('>> ', ">>" + c5 + " ") + invalid_cmd = invalid_cmd.replace(' <<', " " + cr + "<<") + invalid_cmd = invalid_cmd.replace('>>', c7 + ">>" + cr) + invalid_cmd = invalid_cmd.replace('<<', c7 + "<<" + cr) + help_me = True + break + + if help_me: + invalid_run_command(invalid_cmd) + + with open(file_to_print, 'r', encoding='utf-8') as f: + all_code = f.read() + all_code = all_code.replace("\t", " ") + code_lines = all_code.split('\n') + + console_width = None # width of console output when running script + used_width = None # code_width and few spaces on right for padding + magic_console = None + magic_syntax = None + try: + console_width = os.popen('stty size', 'r').read().split()[1] + if console_width: + console_width = int(console_width) + except Exception: + console_width = None + + use_rich = False + if sys.version_info[0] == 3 and sys.version_info[1] >= 6: + use_rich = True + + if use_rich: + from rich.console import Console + from rich.syntax import Syntax + the_code = "\n".join(code_lines) + code_width = 1 + + w = 0 # line number whitespace + if line_numbers: + w = 4 + num_lines = len(code_lines) + if num_lines >= 10: + w = 5 + if num_lines >= 100: + w = 6 + if num_lines >= 1000: + w = 7 + + if is_python_file: + new_sb_lines = [] + for line in code_lines: + line_length2 = len(line) # Normal Python string length used + line_length = get_width(line) # Special characters count 2X + if line_length > code_width: + code_width = line_length + + if console_width: + # If line is larger than console_width, try to optimize it. + # Smart Python word wrap to be used with valid indentation. + if line_length + w > console_width: # 5 is line number ws + if line.count(' # ') == 1: # Has comments like this + if get_width( + line.split( + ' # ')[0]) + w <= console_width: + new_sb_lines.append(line) + continue + elif line.count(' # ') == 1: # Has bad flake8 comment + if get_width( + line.split( + ' # ')[0]) + w <= console_width: + new_sb_lines.append(line) + continue + if line.startswith("from") and " import " in line: + line1 = line.split(" import ")[0] + " \\" + line2 = " import " + line.split(" import ")[1] + new_sb_lines.append(line1) + new_sb_lines.append(line2) + continue + elif line.count('(') == 1 and line.count(')') == 1: + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + line1 = line.split('(')[0] + '(' + line2 = new_ws + line.split('(')[1] + if not ('):') in line2: + new_sb_lines.append(line1) + if get_width(line2) + w > console_width: + if line2.count('", "') == 1: + line2a = line2.split('", "')[0] + '",' + line2b = new_ws + '"' + ( + line2.split('", "')[1]) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + elif line2.count("', '") == 1: + line2a = line2.split("', '")[0] + "'," + line2b = new_ws + "'" + ( + line2.split("', '")[1]) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + elif line2.count("://") == 1 and ( + line2.count('")') == 1): + line2a = line2.split("://")[0] + '://"' + line2b = new_ws + '"' + ( + line2.split("://")[1]) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + elif line2.count("://") == 1 and ( + line2.count("')") == 1): + line2a = line2.split("://")[0] + "://'" + line2b = new_ws + "'" + ( + line2.split("://")[1]) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + elif line2.count('="') == 1 and ( + line2.lstrip().startswith("'")): + line2a = line2.split('="')[0] + "='" + line2b = new_ws + "'\"" + ( + line2.split('="')[1]) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + elif line2.count("='") == 1 and ( + line2.lstrip().startswith('"')): + line2a = line2.split("='")[0] + '="' + line2b = new_ws + '"\'' + ( + line2.split("='")[1]) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + new_sb_lines.append(line2) + elif get_width(line2) + 4 + w <= console_width: + line2 = " " + line2 + new_sb_lines.append(line1) + new_sb_lines.append(line2) + else: + new_sb_lines.append(line) + continue + elif line.count('= "') == 1 and line.count('://') == 1: + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + line1 = line.split('://')[0] + '://" \\' + line2 = new_ws + '"' + line.split('://')[1] + new_sb_lines.append(line1) + if get_width(line2) + w > console_width: + if line2.count('/') > 0: + slash_one = line2.find('/') + line2a = line2[:slash_one+1] + '" \\' + line2b = new_ws + '"' + line2[slash_one+1:] + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + new_sb_lines.append(line2) + continue + elif line.count("= '") == 1 and line.count('://') == 1: + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + line1 = line.split('://')[0] + '://" \\' + line2 = new_ws + "'" + line.split('://')[1] + new_sb_lines.append(line1) + if get_width(line2) + w > console_width: + if line2.count('/') > 0: + slash_one = line2.find('/') + line2a = line2[:slash_one+1] + "' \\" + line2b = new_ws + "'" + line2[slash_one+1:] + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + new_sb_lines.append(line2) + continue + new_sb_lines.append(line) + + if new_sb_lines: + code_lines = new_sb_lines + the_code = "\n".join(code_lines) + + if code_lang != "python": + for line in code_lines: + line_length = get_width(line) + if line_length > code_width: + code_width = line_length + + extra_r_spaces = 2 + if console_width and (code_width + extra_r_spaces < console_width): + used_width = code_width + extra_r_spaces + + magic_syntax = Syntax( + the_code, code_lang, theme="monokai", + line_numbers=line_numbers, code_width=used_width, + word_wrap=word_wrap) + magic_console = Console() + # ---------------------------------------- + dash_length = 62 # May change + if used_width and used_width + w < console_width: + dash_length = used_width + w + elif console_width: + dash_length = console_width + dashes = "-" * dash_length + print(dashes) + print_success = False + if use_rich and code_lang == "markdown": + try: + from rich.markdown import Markdown + markdown = Markdown(all_code) + markdown_console = Console() + markdown_console.print(markdown) # noqa + print_success = True + except Exception: + pass + elif use_rich and magic_syntax: + try: + magic_console.print(magic_syntax) # noqa + print_success = True + except Exception: + pass + if not use_rich or not magic_syntax or not print_success: + for line in code_lines: + print(line) + print(dashes) + # ---------------------------------------- + + +if __name__ == "__main__": + invalid_run_command() diff --git a/seleniumbase/translate/translator.py b/seleniumbase/translate/translator.py index 67b2f662833..77738a790f2 100755 --- a/seleniumbase/translate/translator.py +++ b/seleniumbase/translate/translator.py @@ -14,6 +14,8 @@ -p / --print (Print translation output to the screen) -o / --overwrite (Overwrite the file being translated) -c / --copy (Copy the translation to a new .py file) +Options: + -n (include line Numbers when using the Print action) Output: Translates a SeleniumBase Python file into the language specified. Method calls and "import" lines get swapped. @@ -52,6 +54,8 @@ def invalid_run_command(msg=None): exp += " -p / --print (Print translation output to the screen)\n" exp += " -o / --overwrite (Overwrite the file being translated)\n" exp += " -c / --copy (Copy the translation to a new .py file)\n" + exp += " Options:\n" + exp += " -n (include line Numbers when using the Print action)\n" exp += " Output:\n" exp += " Translates a SeleniumBase Python file into the language\n" exp += ' specified. Method calls and "import" lines get swapped.\n' @@ -68,8 +72,8 @@ def invalid_run_command(msg=None): raise Exception('INVALID RUN COMMAND!\n%s\n\n%s' % (msg, exp)) -def ranges(): - # Get the ranges of special characters of Chinese, Japanese, and Korean +def sc_ranges(): + # Get the ranges of special characters of Chinese, Japanese, and Korean. special_char_ranges = ([ {"from": ord(u"\u3300"), "to": ord(u"\u33ff")}, {"from": ord(u"\ufe30"), "to": ord(u"\ufe4f")}, @@ -89,13 +93,15 @@ def ranges(): def is_cjk(char): - # Returns True if the special character is Chinese, Japanese, or Korean - sc = any([range["from"] <= ord(char) <= range["to"] for range in ranges()]) + # Returns True if the special character is Chinese, Japanese, or Korean. + sc = any( + [range["from"] <= ord(char) <= range["to"] for range in sc_ranges()]) return sc def get_width(line): - # Chinese/Japanese/Korean characters take up double width visually + # Return the true width of the line. Not the same as line length. + # Chinese/Japanese/Korean characters take up double width visually. line_length = len(line) for char in line: if is_cjk(char): @@ -253,6 +259,7 @@ def main(): print_only = False help_me = False invalid_cmd = None + line_numbers = False expected_arg = ("A SeleniumBase Python file") command_args = sys.argv[2:] @@ -284,6 +291,8 @@ def main(): copy = True elif option == "-p" or option == "--print": print_only = True + elif option == "-n": + line_numbers = True elif option == "--en" or option == "--english": new_lang = "English" elif option == "--zh" or option == "--chinese": @@ -444,6 +453,7 @@ def main(): raise Exception("\n\n`%s` is not a valid SeleniumBase test file!\n" "\nExpecting: [%s]\n" % (seleniumbase_file, expected_arg)) + all_code = all_code.replace("\t", " ") code_lines = all_code.split('\n') seleniumbase_lines, changed, d_l = process_test_file(code_lines, new_lang) @@ -482,14 +492,16 @@ def main(): python_code = "\n".join(seleniumbase_lines) code_width = 1 - w = 4 # line number whitespace - num_lines = len(seleniumbase_lines) - if num_lines >= 10: - w = 5 - if num_lines >= 100: - w = 6 - if num_lines >= 1000: - w = 7 + w = 0 # line number whitespace + if line_numbers: + w = 4 + num_lines = len(code_lines) + if num_lines >= 10: + w = 5 + if num_lines >= 100: + w = 6 + if num_lines >= 1000: + w = 7 new_sb_lines = [] for line in seleniumbase_lines: @@ -626,7 +638,8 @@ def main(): magic_syntax = Syntax( python_code, "python", theme="monokai", - line_numbers=True, code_width=used_width, word_wrap=False) + line_numbers=line_numbers, code_width=used_width, + word_wrap=False) magic_console = Console() print("") print(save_line) diff --git a/setup.py b/setup.py index 70e2529822a..0de852c6579 100755 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ setup( name='seleniumbase', - version='1.39.4', + version='1.39.5', description='Fast, Easy, and Reliable Browser Automation & Testing.', long_description=long_description, long_description_content_type='text/markdown', @@ -105,7 +105,7 @@ 'selenium==3.141.0', 'pluggy==0.13.1', 'attrs>=19.3.0', - 'pytest==4.6.10;python_version<"3.5"', + 'pytest==4.6.11;python_version<"3.5"', 'pytest==5.4.3;python_version>="3.5"', 'pytest-cov==2.9.0', 'pytest-forked==1.1.3', @@ -125,6 +125,7 @@ 'pyopenssl==19.1.0', 'pygments==2.5.2;python_version<"3.5"', 'pygments==2.6.1;python_version>="3.5"', + 'livereload==2.6.2', 'colorama==0.4.3', 'brython>=3.8.9', 'pymysql==0.9.3', @@ -132,12 +133,12 @@ 'pyotp==2.3.0', 'boto==2.49.0', 'cffi==1.14.0', - 'rich==1.3.1;python_version>="3.6" and python_version<"4.0"', + 'rich==2.0.0;python_version>="3.6" and python_version<"4.0"', 'flake8==3.7.9;python_version<"3.5"', 'flake8==3.8.2;python_version>="3.5"', 'pyflakes==2.1.1;python_version<"3.5"', 'pyflakes==2.2.0;python_version>="3.5"', - 'certifi>=2020.4.5.1', + 'certifi>=2020.4.5.2', 'pdfminer.six==20191110;python_version<"3.5"', 'pdfminer.six==20200517;python_version>="3.5"', ],