From 6e8e4b85996bfcbd6fc71a461212e98aa5e20115 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Tue, 18 Apr 2017 20:05:03 -0500 Subject: [PATCH 001/627] Switch from pycryptodome to cryptography. --- mycli/config.py | 50 ++++++++++++++++++++++++++++++------------------- setup.py | 2 +- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/mycli/config.py b/mycli/config.py index 7f5e0cb2..88bbc48e 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -6,12 +6,15 @@ from os.path import exists import struct import sys + from configobj import ConfigObj, ConfigObjError +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend + try: basestring except NameError: basestring = str -from Crypto.Cipher import AES logger = logging.getLogger(__name__) @@ -144,7 +147,8 @@ def read_and_decrypt_mylogin_cnf(f): rkey = struct.pack('16B', *rkey) # Create a cipher object using the key. - aes_cipher = AES.new(rkey, AES.MODE_ECB) + aes_cipher = _get_aes_cipher(rkey) + decryptor = aes_cipher.decryptor() # Create a bytes buffer to hold the plaintext. plaintext = BytesIO() @@ -158,24 +162,9 @@ def read_and_decrypt_mylogin_cnf(f): # Read cipher_len bytes from the file and decrypt. cipher = f.read(cipher_len) - pplain = aes_cipher.decrypt(cipher) - - try: - # Determine pad length. - pad_len = ord(pplain[-1:]) - except TypeError: - # ord() was unable to get the value of the byte. - logger.warning('Unable to remove pad.') - continue - - if pad_len > len(pplain) or len(set(pplain[-pad_len:])) != 1: - # Pad length should be less than or equal to the length of the - # plaintext. The pad should have a single unqiue byte. - logger.warning('Invalid pad found in login path file.') + plain = _remove_pad(decryptor.update(cipher)) + if plain is False: continue - - # Get rid of pad. - plain = pplain[:-pad_len] plaintext.write(plain) if plaintext.tell() == 0: @@ -201,3 +190,26 @@ def str_to_bool(s): return False else: raise ValueError('not a recognized boolean value: %s'.format(s)) + +def _get_aes_cipher(key): + """Get the AES cipher object.""" + return Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend()) + +def _remove_pad(line): + """Remove the pad from the *line*.""" + pad_length = ord(line[-1:]) + try: + # Determine pad length. + pad_length = ord(line[-1:]) + except TypeError: + # ord() was unable to get the value of the byte. + logger.warning('Unable to remove pad.') + return False + + if pad_length > len(line) or len(set(line[-pad_length:])) != 1: + # Pad length should be less than or equal to the length of the + # plaintext. The pad should have a single unqiue byte. + logger.warning('Invalid pad found in login path file.') + return False + + return line[:-pad_length] diff --git a/setup.py b/setup.py index 82cdef6e..fb05509e 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ 'PyMySQL >= 0.6.7', 'sqlparse>=0.2.2,<0.3.0', 'configobj >= 5.0.5', - 'pycryptodome >= 3', + 'cryptography >= 1.0.0', ] setup( From 2b049ae41f87ce94218d15f07c2bea97dd3a3335 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Tue, 18 Apr 2017 20:06:20 -0500 Subject: [PATCH 002/627] Pep8radius fixes. --- mycli/config.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/mycli/config.py b/mycli/config.py index 88bbc48e..85c29cd7 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -19,6 +19,7 @@ logger = logging.getLogger(__name__) + def log(logger, level, message): """Logs message to stderr if logging isn't initialized.""" @@ -27,6 +28,7 @@ def log(logger, level, message): else: print(message, file=sys.stderr) + def read_config_file(f): """Read a config file.""" @@ -47,6 +49,7 @@ def read_config_file(f): return config + def read_config_files(files): """Read and merge a list of config files.""" @@ -60,6 +63,7 @@ def read_config_files(files): return config + def write_default_config(source, destination, overwrite=False): destination = os.path.expanduser(destination) if not overwrite and exists(destination): @@ -67,6 +71,7 @@ def write_default_config(source, destination, overwrite=False): shutil.copyfile(source, destination) + def get_mylogin_cnf_path(): """Return the path to the login path file or None if it doesn't exist.""" mylogin_cnf_path = os.getenv('MYSQL_TEST_LOGIN_FILE') @@ -83,6 +88,7 @@ def get_mylogin_cnf_path(): return mylogin_cnf_path return None + def open_mylogin_cnf(name): """Open a readable version of .mylogin.cnf. @@ -105,6 +111,7 @@ def open_mylogin_cnf(name): return TextIOWrapper(plaintext) + def read_and_decrypt_mylogin_cnf(f): """Read and decrypt the contents of .mylogin.cnf. @@ -174,6 +181,7 @@ def read_and_decrypt_mylogin_cnf(f): plaintext.seek(0) return plaintext + def str_to_bool(s): """Convert a string value to its corresponding boolean value.""" if isinstance(s, bool): @@ -191,10 +199,12 @@ def str_to_bool(s): else: raise ValueError('not a recognized boolean value: %s'.format(s)) + def _get_aes_cipher(key): """Get the AES cipher object.""" return Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend()) + def _remove_pad(line): """Remove the pad from the *line*.""" pad_length = ord(line[-1:]) From 7d91c9280fc33bb45d410131f8b55d8bb62b0c93 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Tue, 18 Apr 2017 20:09:18 -0500 Subject: [PATCH 003/627] Add cryptography package change to the changelog. --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 1ae07cfd..43c023f7 100644 --- a/changelog.md +++ b/changelog.md @@ -25,6 +25,7 @@ Internal Changes: * Test mycli using pexpect/python-behave (Thanks: [Dick Marinus]). * Run pep8 checks in travis (Thanks: [Irina Truong]). * Remove temporary hack for sqlparse (Thanks: [Dick Marinus]). +* Switch from pycryptodome to cryptography (Thanks: [Thomas Roten]). 1.9.0: ====== From 6d2d1364d0b4f89d4b8ffcb54878dca97ac7836a Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Tue, 18 Apr 2017 20:16:38 -0500 Subject: [PATCH 004/627] Simplify decryptor/cipher call. --- mycli/config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mycli/config.py b/mycli/config.py index 85c29cd7..4f17d9f6 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -153,9 +153,8 @@ def read_and_decrypt_mylogin_cnf(f): return None rkey = struct.pack('16B', *rkey) - # Create a cipher object using the key. - aes_cipher = _get_aes_cipher(rkey) - decryptor = aes_cipher.decryptor() + # Create a decryptor object using the key. + decryptor = _get_decryptor(rkey) # Create a bytes buffer to hold the plaintext. plaintext = BytesIO() @@ -200,9 +199,10 @@ def str_to_bool(s): raise ValueError('not a recognized boolean value: %s'.format(s)) -def _get_aes_cipher(key): +def _get_decryptor(key): """Get the AES cipher object.""" - return Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend()) + c = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend()) + return c.decryptor() def _remove_pad(line): From 6927717f9c10d87165a25c8c6dc71dcb7c50e609 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Tue, 18 Apr 2017 20:17:30 -0500 Subject: [PATCH 005/627] Fix docstring. --- mycli/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/config.py b/mycli/config.py index 4f17d9f6..daf5695b 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -200,7 +200,7 @@ def str_to_bool(s): def _get_decryptor(key): - """Get the AES cipher object.""" + """Get the AES decryptor.""" c = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend()) return c.decryptor() From 35abc84cbb0b59c8da64a468d8a5774da7689fed Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Fri, 21 Apr 2017 15:07:50 +0200 Subject: [PATCH 006/627] mv authors --- changelog.md | 7 +++++++ AUTHORS => mycli/AUTHORS | 1 + SPONSORS => mycli/SPONSORS | 0 mycli/main.py | 2 +- setup.py | 2 +- tests/test_main.py | 2 +- 6 files changed, 11 insertions(+), 3 deletions(-) rename AUTHORS => mycli/AUTHORS (97%) rename SPONSORS => mycli/SPONSORS (100%) diff --git a/changelog.md b/changelog.md index af5c81fa..701d9dbc 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,10 @@ +TBD +=== + +Internal Changes: +----------------- +* Move AUTHORS and SPONSORS to mycli directory. (Thanks: [Terje Røsten] []). + 1.10.0: ======= diff --git a/AUTHORS b/mycli/AUTHORS similarity index 97% rename from AUTHORS rename to mycli/AUTHORS index 2eb4af10..a7bb7765 100644 --- a/AUTHORS +++ b/mycli/AUTHORS @@ -49,6 +49,7 @@ Contributors: * cxbig * chainkite * Michał Górny + * Terje Røsten Creator: -------- diff --git a/SPONSORS b/mycli/SPONSORS similarity index 100% rename from SPONSORS rename to mycli/SPONSORS diff --git a/mycli/main.py b/mycli/main.py index ff3e8d9c..b24adead 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -439,7 +439,7 @@ def run_cli(self): if self.smart_completion: self.refresh_completions() - project_root = os.path.dirname(PACKAGE_ROOT) + project_root = os.path.join(os.path.dirname(PACKAGE_ROOT), 'mycli') author_file = os.path.join(project_root, 'AUTHORS') sponsor_file = os.path.join(project_root, 'SPONSORS') diff --git a/setup.py b/setup.py index 33ab724f..6e982d9d 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ version=version, url='http://mycli.net', packages=find_packages(), - package_data={'mycli': ['myclirc', '../AUTHORS', '../SPONSORS']}, + package_data={'mycli': ['myclirc', 'AUTHORS', 'SPONSORS']}, description=description, long_description=description, install_requires=install_requirements, diff --git a/tests/test_main.py b/tests/test_main.py index 1271f18d..56c0cb26 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -164,7 +164,7 @@ def test_confirm_destructive_query_notty(executor): assert confirm_destructive_query(sql) is None def test_thanks_picker_utf8(): - project_root = os.path.dirname(PACKAGE_ROOT) + project_root = os.path.join(os.path.dirname(PACKAGE_ROOT), 'mycli') author_file = os.path.join(project_root, 'AUTHORS') sponsor_file = os.path.join(project_root, 'SPONSORS') From 4f77ec7a548ffd787b63c9916c6d955905e20a07 Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Sat, 22 Apr 2017 20:18:21 +0200 Subject: [PATCH 007/627] rename tests/ to test/ rename tests/ to test so it will be included in sdist see: Anything that looks like a test script: test/test*.py (currently, the Distutils don't do anything with test scripts except include them in source distributions, but in the future there will be a standard for testing Python module distributions) https://docs.python.org/3/distutils/sourcedist.html --- .gitignore | 2 +- .travis.yml | 2 +- {tests => test}/conftest.py | 0 {tests => test}/features/basic_commands.feature | 0 {tests => test}/features/crud_database.feature | 0 {tests => test}/features/crud_table.feature | 0 {tests => test}/features/db_utils.py | 0 {tests => test}/features/environment.py | 0 {tests => test}/features/fixture_data/help.txt | 0 .../features/fixture_data/help_commands.txt | 0 {tests => test}/features/fixture_utils.py | 0 {tests => test}/features/iocommands.feature | 0 {tests => test}/features/named_queries.feature | 0 {tests => test}/features/specials.feature | 0 {tests => test}/features/steps/basic_commands.py | 0 {tests => test}/features/steps/crud_database.py | 0 {tests => test}/features/steps/crud_table.py | 0 {tests => test}/features/steps/iocommands.py | 0 {tests => test}/features/steps/named_queries.py | 0 {tests => test}/features/steps/specials.py | 0 {tests => test}/features/steps/wrappers.py | 0 {tests => test}/mylogin.cnf | Bin {tests => test}/test.txt | 0 {tests => test}/test_completion_engine.py | 0 {tests => test}/test_completion_refresher.py | 0 {tests => test}/test_config.py | 0 {tests => test}/test_dbspecial.py | 0 {tests => test}/test_expanded.py | 0 {tests => test}/test_main.py | 0 {tests => test}/test_naive_completion.py | 0 {tests => test}/test_output_formatter.py | 0 {tests => test}/test_parseutils.py | 0 {tests => test}/test_plan.wiki | 0 .../test_smart_completion_public_schema_only.py | 0 {tests => test}/test_special_iocommands.py | 0 {tests => test}/test_sqlexecute.py | 8 ++++---- {tests => test}/test_tabulate.py | 0 {tests => test}/utils.py | 0 38 files changed, 6 insertions(+), 6 deletions(-) rename {tests => test}/conftest.py (100%) rename {tests => test}/features/basic_commands.feature (100%) rename {tests => test}/features/crud_database.feature (100%) rename {tests => test}/features/crud_table.feature (100%) rename {tests => test}/features/db_utils.py (100%) rename {tests => test}/features/environment.py (100%) rename {tests => test}/features/fixture_data/help.txt (100%) rename {tests => test}/features/fixture_data/help_commands.txt (100%) rename {tests => test}/features/fixture_utils.py (100%) rename {tests => test}/features/iocommands.feature (100%) rename {tests => test}/features/named_queries.feature (100%) rename {tests => test}/features/specials.feature (100%) rename {tests => test}/features/steps/basic_commands.py (100%) rename {tests => test}/features/steps/crud_database.py (100%) rename {tests => test}/features/steps/crud_table.py (100%) rename {tests => test}/features/steps/iocommands.py (100%) rename {tests => test}/features/steps/named_queries.py (100%) rename {tests => test}/features/steps/specials.py (100%) rename {tests => test}/features/steps/wrappers.py (100%) rename {tests => test}/mylogin.cnf (100%) rename {tests => test}/test.txt (100%) rename {tests => test}/test_completion_engine.py (100%) rename {tests => test}/test_completion_refresher.py (100%) rename {tests => test}/test_config.py (100%) rename {tests => test}/test_dbspecial.py (100%) rename {tests => test}/test_expanded.py (100%) rename {tests => test}/test_main.py (100%) rename {tests => test}/test_naive_completion.py (100%) rename {tests => test}/test_output_formatter.py (100%) rename {tests => test}/test_parseutils.py (100%) rename {tests => test}/test_plan.wiki (100%) rename {tests => test}/test_smart_completion_public_schema_only.py (100%) rename {tests => test}/test_special_iocommands.py (100%) rename {tests => test}/test_sqlexecute.py (97%) rename {tests => test}/test_tabulate.py (100%) rename {tests => test}/utils.py (100%) diff --git a/.gitignore b/.gitignore index 59fa76be..e907154f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ /dist /mycli.egg-info /src -/tests/behave.ini +/test/behave.ini .vagrant *.pyc diff --git a/.travis.yml b/.travis.yml index f78d94b0..021be1c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ install: script: - coverage run --source mycli -m py.test - - cd tests + - cd test - behave - cd .. # check for pep8 errors, only looking at branch vs master. If there are errors, show diff and return an error code. diff --git a/tests/conftest.py b/test/conftest.py similarity index 100% rename from tests/conftest.py rename to test/conftest.py diff --git a/tests/features/basic_commands.feature b/test/features/basic_commands.feature similarity index 100% rename from tests/features/basic_commands.feature rename to test/features/basic_commands.feature diff --git a/tests/features/crud_database.feature b/test/features/crud_database.feature similarity index 100% rename from tests/features/crud_database.feature rename to test/features/crud_database.feature diff --git a/tests/features/crud_table.feature b/test/features/crud_table.feature similarity index 100% rename from tests/features/crud_table.feature rename to test/features/crud_table.feature diff --git a/tests/features/db_utils.py b/test/features/db_utils.py similarity index 100% rename from tests/features/db_utils.py rename to test/features/db_utils.py diff --git a/tests/features/environment.py b/test/features/environment.py similarity index 100% rename from tests/features/environment.py rename to test/features/environment.py diff --git a/tests/features/fixture_data/help.txt b/test/features/fixture_data/help.txt similarity index 100% rename from tests/features/fixture_data/help.txt rename to test/features/fixture_data/help.txt diff --git a/tests/features/fixture_data/help_commands.txt b/test/features/fixture_data/help_commands.txt similarity index 100% rename from tests/features/fixture_data/help_commands.txt rename to test/features/fixture_data/help_commands.txt diff --git a/tests/features/fixture_utils.py b/test/features/fixture_utils.py similarity index 100% rename from tests/features/fixture_utils.py rename to test/features/fixture_utils.py diff --git a/tests/features/iocommands.feature b/test/features/iocommands.feature similarity index 100% rename from tests/features/iocommands.feature rename to test/features/iocommands.feature diff --git a/tests/features/named_queries.feature b/test/features/named_queries.feature similarity index 100% rename from tests/features/named_queries.feature rename to test/features/named_queries.feature diff --git a/tests/features/specials.feature b/test/features/specials.feature similarity index 100% rename from tests/features/specials.feature rename to test/features/specials.feature diff --git a/tests/features/steps/basic_commands.py b/test/features/steps/basic_commands.py similarity index 100% rename from tests/features/steps/basic_commands.py rename to test/features/steps/basic_commands.py diff --git a/tests/features/steps/crud_database.py b/test/features/steps/crud_database.py similarity index 100% rename from tests/features/steps/crud_database.py rename to test/features/steps/crud_database.py diff --git a/tests/features/steps/crud_table.py b/test/features/steps/crud_table.py similarity index 100% rename from tests/features/steps/crud_table.py rename to test/features/steps/crud_table.py diff --git a/tests/features/steps/iocommands.py b/test/features/steps/iocommands.py similarity index 100% rename from tests/features/steps/iocommands.py rename to test/features/steps/iocommands.py diff --git a/tests/features/steps/named_queries.py b/test/features/steps/named_queries.py similarity index 100% rename from tests/features/steps/named_queries.py rename to test/features/steps/named_queries.py diff --git a/tests/features/steps/specials.py b/test/features/steps/specials.py similarity index 100% rename from tests/features/steps/specials.py rename to test/features/steps/specials.py diff --git a/tests/features/steps/wrappers.py b/test/features/steps/wrappers.py similarity index 100% rename from tests/features/steps/wrappers.py rename to test/features/steps/wrappers.py diff --git a/tests/mylogin.cnf b/test/mylogin.cnf similarity index 100% rename from tests/mylogin.cnf rename to test/mylogin.cnf diff --git a/tests/test.txt b/test/test.txt similarity index 100% rename from tests/test.txt rename to test/test.txt diff --git a/tests/test_completion_engine.py b/test/test_completion_engine.py similarity index 100% rename from tests/test_completion_engine.py rename to test/test_completion_engine.py diff --git a/tests/test_completion_refresher.py b/test/test_completion_refresher.py similarity index 100% rename from tests/test_completion_refresher.py rename to test/test_completion_refresher.py diff --git a/tests/test_config.py b/test/test_config.py similarity index 100% rename from tests/test_config.py rename to test/test_config.py diff --git a/tests/test_dbspecial.py b/test/test_dbspecial.py similarity index 100% rename from tests/test_dbspecial.py rename to test/test_dbspecial.py diff --git a/tests/test_expanded.py b/test/test_expanded.py similarity index 100% rename from tests/test_expanded.py rename to test/test_expanded.py diff --git a/tests/test_main.py b/test/test_main.py similarity index 100% rename from tests/test_main.py rename to test/test_main.py diff --git a/tests/test_naive_completion.py b/test/test_naive_completion.py similarity index 100% rename from tests/test_naive_completion.py rename to test/test_naive_completion.py diff --git a/tests/test_output_formatter.py b/test/test_output_formatter.py similarity index 100% rename from tests/test_output_formatter.py rename to test/test_output_formatter.py diff --git a/tests/test_parseutils.py b/test/test_parseutils.py similarity index 100% rename from tests/test_parseutils.py rename to test/test_parseutils.py diff --git a/tests/test_plan.wiki b/test/test_plan.wiki similarity index 100% rename from tests/test_plan.wiki rename to test/test_plan.wiki diff --git a/tests/test_smart_completion_public_schema_only.py b/test/test_smart_completion_public_schema_only.py similarity index 100% rename from tests/test_smart_completion_public_schema_only.py rename to test/test_smart_completion_public_schema_only.py diff --git a/tests/test_special_iocommands.py b/test/test_special_iocommands.py similarity index 100% rename from tests/test_special_iocommands.py rename to test/test_special_iocommands.py diff --git a/tests/test_sqlexecute.py b/test/test_sqlexecute.py similarity index 97% rename from tests/test_sqlexecute.py rename to test/test_sqlexecute.py index a9b5fbec..157b6234 100644 --- a/tests/test_sqlexecute.py +++ b/test/test_sqlexecute.py @@ -229,7 +229,7 @@ def test_system_command_not_found(executor): @dbtest def test_system_command_output(executor): - test_file_path = os.path.join(os.path.abspath('.'), 'tests/test.txt') + test_file_path = os.path.join(os.path.abspath('.'), 'test', 'test.txt') results = run(executor, 'system cat {0}'.format(test_file_path)) assert len(results) == 1 expected_line = u'mycli rocks!\n' @@ -237,9 +237,9 @@ def test_system_command_output(executor): @dbtest def test_cd_command_current_dir(executor): - tests_path = os.path.join(os.path.abspath('.'), 'tests') - results = run(executor, 'system cd {0}'.format(tests_path)) - assert os.getcwd() == tests_path + test_path = os.path.join(os.path.abspath('.'), 'test') + results = run(executor, 'system cd {0}'.format(test_path)) + assert os.getcwd() == test_path @dbtest def test_unicode_support(executor): diff --git a/tests/test_tabulate.py b/test/test_tabulate.py similarity index 100% rename from tests/test_tabulate.py rename to test/test_tabulate.py diff --git a/tests/utils.py b/test/utils.py similarity index 100% rename from tests/utils.py rename to test/utils.py From 9e20cc6711c89696843363e8c090abf2be235425 Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Sat, 22 Apr 2017 12:48:37 +0200 Subject: [PATCH 008/627] add more files to MANIFEST.in --- MANIFEST.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 1d3bbc86..a50b7368 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ -include LICENSE.txt *.md +include LICENSE.txt *.md *.rst TODO requirements-dev.txt screenshots/* +include conftest.py .coveragerc pytest.ini test tox.ini From 26782c77520d9d0dc08a4820abf5450b90ca0846 Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Sat, 22 Apr 2017 13:08:25 +0200 Subject: [PATCH 009/627] add to changelog.md --- changelog.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/changelog.md b/changelog.md index af5c81fa..f00fc435 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,12 @@ +TBD +=== + +Internal Changes: +----------------- + +* Rename tests/ to test/ . (Thanks: [Dick Marinus]). + + 1.10.0: ======= From a9af5d41a4b0e079776a5baf7c214196dbfc57a3 Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Sat, 22 Apr 2017 20:08:31 +0200 Subject: [PATCH 010/627] fix pep8 code style --- test/conftest.py | 3 +- test/features/db_utils.py | 27 +-- test/features/environment.py | 17 +- test/features/fixture_utils.py | 9 +- test/features/steps/basic_commands.py | 33 ++-- test/features/steps/crud_database.py | 60 +++---- test/features/steps/crud_table.py | 63 +++---- test/features/steps/iocommands.py | 7 +- test/features/steps/named_queries.py | 33 ++-- test/features/steps/specials.py | 20 +-- test/features/steps/wrappers.py | 3 +- test/test_completion_engine.py | 170 +++++++++++------- test/test_completion_refresher.py | 15 +- test/test_config.py | 2 +- test/test_dbspecial.py | 3 +- test/test_main.py | 9 + test/test_naive_completion.py | 6 + test/test_parseutils.py | 21 ++- ...est_smart_completion_public_schema_only.py | 79 +++++--- test/test_special_iocommands.py | 15 +- test/test_sqlexecute.py | 30 +++- test/utils.py | 13 +- 22 files changed, 369 insertions(+), 269 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index d24d26bc..d7d13340 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,5 +1,6 @@ import pytest -from utils import (HOST, USER, PASSWORD, PORT, CHARSET, create_db, db_connection) +from utils import (HOST, USER, PASSWORD, PORT, + CHARSET, create_db, db_connection) import mycli.sqlexecute diff --git a/test/features/db_utils.py b/test/features/db_utils.py index f604c608..ef0b42ff 100644 --- a/test/features/db_utils.py +++ b/test/features/db_utils.py @@ -4,15 +4,17 @@ import pymysql + def create_db(hostname='localhost', username=None, password=None, dbname=None): - """ - Create test database. + """Create test database. + :param hostname: string :param username: string :param password: string :param dbname: string :return: + """ cn = pymysql.connect( host=hostname, @@ -23,8 +25,8 @@ def create_db(hostname='localhost', username=None, password=None, ) with cn.cursor() as cr: - cr.execute('drop database if exists '+dbname) - cr.execute('create database '+dbname) + cr.execute('drop database if exists ' + dbname) + cr.execute('create database ' + dbname) cn.close() @@ -33,13 +35,14 @@ def create_db(hostname='localhost', username=None, password=None, def create_cn(hostname, password, username, dbname): - """ - Open connection to database. + """Open connection to database. + :param hostname: :param password: :param username: :param dbname: string :return: psycopg2.connection + """ cn = pymysql.connect( host=hostname, @@ -55,12 +58,13 @@ def create_cn(hostname, password, username, dbname): def drop_db(hostname='localhost', username=None, password=None, dbname=None): - """ - Drop database. + """Drop database. + :param hostname: string :param username: string :param password: string :param dbname: string + """ cn = pymysql.connect( host=hostname, @@ -72,15 +76,16 @@ def drop_db(hostname='localhost', username=None, password=None, ) with cn.cursor() as cr: - cr.execute('drop database if exists '+dbname) + cr.execute('drop database if exists ' + dbname) close_cn(cn) def close_cn(cn=None): - """ - Close connection. + """Close connection. + :param connection: pymysql.connection + """ if cn: cn.close() diff --git a/test/features/environment.py b/test/features/environment.py index 3f55757b..e79e5740 100644 --- a/test/features/environment.py +++ b/test/features/environment.py @@ -9,9 +9,7 @@ def before_all(context): - """ - Set env parameters. - """ + """Set env parameters.""" os.environ['LINES'] = "100" os.environ['COLUMNS'] = "100" os.environ['PAGER'] = 'cat' @@ -21,7 +19,8 @@ def before_all(context): context.exit_sent = False vi = '_'.join([str(x) for x in sys.version_info[:3]]) - db_name = context.config.userdata.get('my_test_db', None) or "mycli_behave_tests" + db_name = context.config.userdata.get( + 'my_test_db', None) or "mycli_behave_tests" db_name_full = '{0}_{1}'.format(db_name, vi) # Store get params from config/environment variables @@ -40,7 +39,7 @@ def before_all(context): ), 'cli_command': context.config.userdata.get( 'my_cli_command', None) or - sys.executable+' -c "import coverage ; coverage.process_startup(); import mycli.main; mycli.main.cli()"', + sys.executable + ' -c "import coverage ; coverage.process_startup(); import mycli.main; mycli.main.cli()"', 'dbname': db_name, 'dbname_tmp': db_name_full + '_tmp', 'vi': vi, @@ -54,9 +53,7 @@ def before_all(context): def after_all(context): - """ - Unset env parameters. - """ + """Unset env parameters.""" dbutils.close_cn(context.cn) dbutils.drop_db(context.conf['host'], context.conf['user'], context.conf['pass'], context.conf['dbname']) @@ -70,9 +67,7 @@ def after_all(context): def after_scenario(context, _): - """ - Cleans up after each test complete. - """ + """Cleans up after each test complete.""" if hasattr(context, 'cli') and not context.exit_sent: # Terminate nicely. diff --git a/test/features/fixture_utils.py b/test/features/fixture_utils.py index f3b490c4..a171e34c 100644 --- a/test/features/fixture_utils.py +++ b/test/features/fixture_utils.py @@ -6,10 +6,11 @@ def read_fixture_lines(filename): - """ - Read lines of text from file. + """Read lines of text from file. + :param filename: string name :return: list of strings + """ lines = [] for line in io.open(filename, 'r', encoding='utf8'): @@ -18,9 +19,7 @@ def read_fixture_lines(filename): def read_fixture_files(): - """ - Read all files inside fixture_data directory. - """ + """Read all files inside fixture_data directory.""" fixture_dict = {} current_dir = os.path.dirname(__file__) diff --git a/test/features/steps/basic_commands.py b/test/features/steps/basic_commands.py index 7aa47664..109472b8 100644 --- a/test/features/steps/basic_commands.py +++ b/test/features/steps/basic_commands.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -""" -Steps for behavioral style tests are defined in this module. -Each step is defined by the string decorating it. -This string is used to call the step in "*.feature" file. +"""Steps for behavioral style tests are defined in this module. + +Each step is defined by the string decorating it. This string is used +to call the step in "*.feature" file. + """ from __future__ import unicode_literals @@ -14,9 +15,7 @@ @when('we run dbcli') def step_run_cli(context): - """ - Run the process using pexpect. - """ + """Run the process using pexpect.""" run_args = [] if context.conf.get('host', None): run_args.extend(('-h', context.conf['host'])) @@ -26,7 +25,8 @@ def step_run_cli(context): run_args.extend(('-p', context.conf['pass'])) if context.conf.get('dbname', None): run_args.extend(('-D', context.conf['dbname'])) - cli_cmd = context.conf.get('cli_command', None) or sys.executable+' -c "import coverage ; coverage.process_startup(); import mycli.main; mycli.main.cli()"' + cli_cmd = context.conf.get('cli_command', None) or sys.executable + \ + ' -c "import coverage ; coverage.process_startup(); import mycli.main; mycli.main.cli()"' cmd_parts = [cli_cmd] + run_args cmd = ' '.join(cmd_parts) @@ -36,27 +36,26 @@ def step_run_cli(context): @when('we wait for prompt') def step_wait_prompt(context): - """ - Make sure prompt is displayed. - """ + """Make sure prompt is displayed.""" user = context.conf['user'] host = context.conf['host'] dbname = context.conf['dbname'] - wrappers.expect_exact(context, 'mysql {0}@{1}:{2}> '.format(user, host, dbname), timeout=5) + wrappers.expect_exact(context, 'mysql {0}@{1}:{2}> '.format( + user, host, dbname), timeout=5) @when('we send "ctrl + d"') def step_ctrl_d(context): - """ - Send Ctrl + D to hopefully exit. - """ + """Send Ctrl + D to hopefully exit.""" context.cli.sendcontrol('d') context.exit_sent = True @when('we send "\?" command') def step_send_help(context): - """ - Send \? to see help. + """Send \? + + to see help. + """ context.cli.sendline('\\?') diff --git a/test/features/steps/crud_database.py b/test/features/steps/crud_database.py index 3eab34d9..d7b8eeb2 100644 --- a/test/features/steps/crud_database.py +++ b/test/features/steps/crud_database.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- -""" -Steps for behavioral style tests are defined in this module. -Each step is defined by the string decorating it. -This string is used to call the step in "*.feature" file. +"""Steps for behavioral style tests are defined in this module. + +Each step is defined by the string decorating it. This string is used +to call the step in "*.feature" file. + """ from __future__ import unicode_literals @@ -14,9 +15,7 @@ @when('we create database') def step_db_create(context): - """ - Send create database. - """ + """Send create database.""" context.cli.sendline('create database {0};'.format( context.conf['dbname_tmp'])) @@ -27,78 +26,67 @@ def step_db_create(context): @when('we drop database') def step_db_drop(context): - """ - Send drop database. - """ + """Send drop database.""" context.cli.sendline('drop database {0};'.format( context.conf['dbname_tmp'])) - wrappers.expect_exact(context, 'You\'re about to run a destructive command.\r\nDo you want to proceed? (y/n):', timeout=2) + wrappers.expect_exact( + context, 'You\'re about to run a destructive command.\r\nDo you want to proceed? (y/n):', timeout=2) context.cli.sendline('y') + @when('we connect to test database') def step_db_connect_test(context): - """ - Send connect to database. - """ + """Send connect to database.""" db_name = context.conf['dbname'] context.cli.sendline('use {0}'.format(db_name)) @when('we connect to dbserver') def step_db_connect_dbserver(context): - """ - Send connect to database. - """ + """Send connect to database.""" context.cli.sendline('use mysql') @then('dbcli exits') def step_wait_exit(context): - """ - Make sure the cli exits. - """ + """Make sure the cli exits.""" wrappers.expect_exact(context, pexpect.EOF, timeout=5) @then('we see dbcli prompt') def step_see_prompt(context): - """ - Wait to see the prompt. - """ + """Wait to see the prompt.""" user = context.conf['user'] host = context.conf['host'] dbname = context.conf['dbname'] - wrappers.expect_exact(context, 'mysql {0}@{1}:{2}> '.format(user, host, dbname), timeout=5) + wrappers.expect_exact(context, 'mysql {0}@{1}:{2}> '.format( + user, host, dbname), timeout=5) @then('we see help output') def step_see_help(context): for expected_line in context.fixture_data['help_commands.txt']: - wrappers.expect_exact(context, expected_line+'\r\n', timeout=1) + wrappers.expect_exact(context, expected_line + '\r\n', timeout=1) @then('we see database created') def step_see_db_created(context): - """ - Wait to see create database output. - """ + """Wait to see create database output.""" wrappers.expect_exact(context, 'Query OK, 1 row affected\r\n', timeout=2) @then('we see database dropped') def step_see_db_dropped(context): - """ - Wait to see drop database output. - """ + """Wait to see drop database output.""" wrappers.expect_exact(context, 'Query OK, 0 rows affected\r\n', timeout=2) @then('we see database connected') def step_see_db_connected(context): - """ - Wait to see drop database output. - """ - wrappers.expect_exact(context, 'You are now connected to database "', timeout=2) + """Wait to see drop database output.""" + wrappers.expect_exact( + context, 'You are now connected to database "', timeout=2) wrappers.expect_exact(context, '"', timeout=2) - wrappers.expect_exact(context, ' as user "{0}"\r\n'.format(context.conf['user']), timeout=2) + wrappers.expect_exact(context, ' as user "{0}"\r\n'.format( + context.conf['user']), timeout=2) diff --git a/test/features/steps/crud_table.py b/test/features/steps/crud_table.py index b73ff0d6..34301c89 100644 --- a/test/features/steps/crud_table.py +++ b/test/features/steps/crud_table.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -""" -Steps for behavioral style tests are defined in this module. -Each step is defined by the string decorating it. -This string is used to call the step in "*.feature" file. +"""Steps for behavioral style tests are defined in this module. + +Each step is defined by the string decorating it. This string is used +to call the step in "*.feature" file. + """ from __future__ import unicode_literals @@ -12,100 +13,78 @@ @when('we create table') def step_create_table(context): - """ - Send create table. - """ + """Send create table.""" context.cli.sendline('create table a(x text);') @when('we insert into table') def step_insert_into_table(context): - """ - Send insert into table. - """ + """Send insert into table.""" context.cli.sendline('''insert into a(x) values('xxx');''') @when('we update table') def step_update_table(context): - """ - Send insert into table. - """ + """Send insert into table.""" context.cli.sendline('''update a set x = 'yyy' where x = 'xxx';''') @when('we select from table') def step_select_from_table(context): - """ - Send select from table. - """ + """Send select from table.""" context.cli.sendline('select * from a;') @when('we delete from table') def step_delete_from_table(context): - """ - Send deete from table. - """ + """Send deete from table.""" context.cli.sendline('''delete from a where x = 'yyy';''') - wrappers.expect_exact(context, 'You\'re about to run a destructive command.\r\nDo you want to proceed? (y/n):', timeout=2) + wrappers.expect_exact( + context, 'You\'re about to run a destructive command.\r\nDo you want to proceed? (y/n):', timeout=2) context.cli.sendline('y') @when('we drop table') def step_drop_table(context): - """ - Send drop table. - """ + """Send drop table.""" context.cli.sendline('drop table a;') - wrappers.expect_exact(context, 'You\'re about to run a destructive command.\r\nDo you want to proceed? (y/n):', timeout=2) + wrappers.expect_exact( + context, 'You\'re about to run a destructive command.\r\nDo you want to proceed? (y/n):', timeout=2) context.cli.sendline('y') @then('we see table created') def step_see_table_created(context): - """ - Wait to see create table output. - """ + """Wait to see create table output.""" wrappers.expect_exact(context, 'Query OK, 0 rows affected\r\n', timeout=2) @then('we see record inserted') def step_see_record_inserted(context): - """ - Wait to see insert output. - """ + """Wait to see insert output.""" wrappers.expect_exact(context, 'Query OK, 1 row affected\r\n', timeout=2) @then('we see record updated') def step_see_record_updated(context): - """ - Wait to see update output. - """ + """Wait to see update output.""" wrappers.expect_exact(context, 'Query OK, 1 row affected\r\n', timeout=2) @then('we see data selected') def step_see_data_selected(context): - """ - Wait to see select output. - """ + """Wait to see select output.""" wrappers.expect_exact( context, '+-----+\r\n| x |\r\n+-----+\r\n| yyy |\r\n+-----+\r\n1 row in set\r\n', timeout=1) @then('we see record deleted') def step_see_data_deleted(context): - """ - Wait to see delete output. - """ + """Wait to see delete output.""" wrappers.expect_exact(context, 'Query OK, 1 row affected\r\n', timeout=2) @then('we see table dropped') def step_see_table_dropped(context): - """ - Wait to see drop output. - """ + """Wait to see drop output.""" wrappers.expect_exact(context, 'Query OK, 0 rows affected\r\n', timeout=2) diff --git a/test/features/steps/iocommands.py b/test/features/steps/iocommands.py index 88520046..73068fac 100644 --- a/test/features/steps/iocommands.py +++ b/test/features/steps/iocommands.py @@ -8,14 +8,13 @@ @when('we start external editor providing a file name') def step_edit_file(context): - """ - Edit file with external editor. - """ + """Edit file with external editor.""" context.editor_file_name = 'test_file_{0}.sql'.format(context.conf['vi']) if os.path.exists(context.editor_file_name): os.remove(context.editor_file_name) context.cli.sendline('\e {0}'.format(context.editor_file_name)) - wrappers.expect_exact(context, 'Entering Ex mode. Type "visual" to go to Normal mode.', timeout=2) + wrappers.expect_exact( + context, 'Entering Ex mode. Type "visual" to go to Normal mode.', timeout=2) wrappers.expect_exact(context, '\r\n:', timeout=2) diff --git a/test/features/steps/named_queries.py b/test/features/steps/named_queries.py index b53ad47d..60115c5c 100644 --- a/test/features/steps/named_queries.py +++ b/test/features/steps/named_queries.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -""" -Steps for behavioral style tests are defined in this module. -Each step is defined by the string decorating it. -This string is used to call the step in "*.feature" file. +"""Steps for behavioral style tests are defined in this module. + +Each step is defined by the string decorating it. This string is used +to call the step in "*.feature" file. + """ from __future__ import unicode_literals @@ -12,48 +13,36 @@ @when('we save a named query') def step_save_named_query(context): - """ - Send \ns command - """ + """Send \ns command.""" context.cli.sendline('\\fs foo SELECT 12345') @when('we use a named query') def step_use_named_query(context): - """ - Send \n command - """ + """Send \n command.""" context.cli.sendline('\\f foo') @when('we delete a named query') def step_delete_named_query(context): - """ - Send \nd command - """ + """Send \nd command.""" context.cli.sendline('\\fd foo') @then('we see the named query saved') def step_see_named_query_saved(context): - """ - Wait to see query saved. - """ + """Wait to see query saved.""" wrappers.expect_exact(context, 'Saved.', timeout=1) @then('we see the named query executed') def step_see_named_query_executed(context): - """ - Wait to see select output. - """ + """Wait to see select output.""" wrappers.expect_exact(context, '12345', timeout=1) wrappers.expect_exact(context, 'SELECT 1', timeout=1) @then('we see the named query deleted') def step_see_named_query_deleted(context): - """ - Wait to see query deleted. - """ + """Wait to see query deleted.""" wrappers.expect_exact(context, 'foo: Deleted', timeout=1) diff --git a/test/features/steps/specials.py b/test/features/steps/specials.py index 790b2476..c0a3c0fe 100644 --- a/test/features/steps/specials.py +++ b/test/features/steps/specials.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -""" -Steps for behavioral style tests are defined in this module. -Each step is defined by the string decorating it. -This string is used to call the step in "*.feature" file. +"""Steps for behavioral style tests are defined in this module. + +Each step is defined by the string decorating it. This string is used +to call the step in "*.feature" file. + """ from __future__ import unicode_literals @@ -12,15 +13,12 @@ @when('we refresh completions') def step_refresh_completions(context): - """ - Send refresh command. - """ + """Send refresh command.""" context.cli.sendline('rehash') @then('we see completions refresh started') def step_see_refresh_started(context): - """ - Wait to see refresh output. - """ - wrappers.expect_exact(context, 'Auto-completion refresh started in the background', timeout=2) + """Wait to see refresh output.""" + wrappers.expect_exact( + context, 'Auto-completion refresh started in the background', timeout=2) diff --git a/test/features/steps/wrappers.py b/test/features/steps/wrappers.py index eac7c830..aea74203 100644 --- a/test/features/steps/wrappers.py +++ b/test/features/steps/wrappers.py @@ -9,7 +9,8 @@ def expect_exact(context, expected, timeout): context.cli.expect_exact(expected, timeout=timeout) except: # Strip color codes out of the output. - actual = re.sub(r'\x1b\[([0-9A-Za-z;?])+[m|K]?', '', context.cli.before) + actual = re.sub(r'\x1b\[([0-9A-Za-z;?])+[m|K]?', + '', context.cli.before) raise Exception('Expected:\n---\n{0!r}\n---\n\nActual:\n---\n{1!r}\n---'.format( expected, actual)) diff --git a/test/test_completion_engine.py b/test/test_completion_engine.py index 9b877108..4f0406b0 100644 --- a/test/test_completion_engine.py +++ b/test/test_completion_engine.py @@ -1,25 +1,28 @@ from mycli.packages.completion_engine import suggest_type import pytest + def sorted_dicts(dicts): - """input is a list of dicts""" + """input is a list of dicts.""" return sorted(tuple(x.items()) for x in dicts) + def test_select_suggests_cols_with_visible_table_scope(): suggestions = suggest_type('SELECT FROM tabl', 'SELECT ') assert sorted_dicts(suggestions) == sorted_dicts([ - {'type': 'column', 'tables': [(None, 'tabl', None)]}, - {'type': 'function', 'schema': []}, - {'type': 'keyword'}, - ]) + {'type': 'column', 'tables': [(None, 'tabl', None)]}, + {'type': 'function', 'schema': []}, + {'type': 'keyword'}, + ]) + def test_select_suggests_cols_with_qualified_table_scope(): suggestions = suggest_type('SELECT FROM sch.tabl', 'SELECT ') assert sorted_dicts(suggestions) == sorted_dicts([ - {'type': 'column', 'tables': [('sch', 'tabl', None)]}, - {'type': 'function', 'schema': []}, - {'type': 'keyword'}, - ]) + {'type': 'column', 'tables': [('sch', 'tabl', None)]}, + {'type': 'function', 'schema': []}, + {'type': 'keyword'}, + ]) @pytest.mark.parametrize('expression', [ @@ -37,10 +40,11 @@ def test_select_suggests_cols_with_qualified_table_scope(): def test_where_suggests_columns_functions(expression): suggestions = suggest_type(expression, expression) assert sorted_dicts(suggestions) == sorted_dicts([ - {'type': 'column', 'tables': [(None, 'tabl', None)]}, - {'type': 'function', 'schema': []}, - {'type': 'keyword'}, - ]) + {'type': 'column', 'tables': [(None, 'tabl', None)]}, + {'type': 'function', 'schema': []}, + {'type': 'keyword'}, + ]) + @pytest.mark.parametrize('expression', [ 'SELECT * FROM tabl WHERE foo IN (', @@ -49,41 +53,49 @@ def test_where_suggests_columns_functions(expression): def test_where_in_suggests_columns(expression): suggestions = suggest_type(expression, expression) assert sorted_dicts(suggestions) == sorted_dicts([ - {'type': 'column', 'tables': [(None, 'tabl', None)]}, - {'type': 'function', 'schema': []}, - {'type': 'keyword'}, - ]) + {'type': 'column', 'tables': [(None, 'tabl', None)]}, + {'type': 'function', 'schema': []}, + {'type': 'keyword'}, + ]) + def test_where_equals_any_suggests_columns_or_keywords(): text = 'SELECT * FROM tabl WHERE foo = ANY(' suggestions = suggest_type(text, text) assert sorted_dicts(suggestions) == sorted_dicts([ - {'type': 'column', 'tables': [(None, 'tabl', None)]}, - {'type': 'function', 'schema': []}, - {'type': 'keyword'}]) + {'type': 'column', 'tables': [(None, 'tabl', None)]}, + {'type': 'function', 'schema': []}, + {'type': 'keyword'}]) + def test_lparen_suggests_cols(): suggestion = suggest_type('SELECT MAX( FROM tbl', 'SELECT MAX(') assert suggestion == [ {'type': 'column', 'tables': [(None, 'tbl', None)]}] + def test_operand_inside_function_suggests_cols1(): - suggestion = suggest_type('SELECT MAX(col1 + FROM tbl', 'SELECT MAX(col1 + ') + suggestion = suggest_type( + 'SELECT MAX(col1 + FROM tbl', 'SELECT MAX(col1 + ') assert suggestion == [ {'type': 'column', 'tables': [(None, 'tbl', None)]}] + def test_operand_inside_function_suggests_cols2(): - suggestion = suggest_type('SELECT MAX(col1 + col2 + FROM tbl', 'SELECT MAX(col1 + col2 + ') + suggestion = suggest_type( + 'SELECT MAX(col1 + col2 + FROM tbl', 'SELECT MAX(col1 + col2 + ') assert suggestion == [ {'type': 'column', 'tables': [(None, 'tbl', None)]}] + def test_select_suggests_cols_and_funcs(): suggestions = suggest_type('SELECT ', 'SELECT ') assert sorted_dicts(suggestions) == sorted_dicts([ - {'type': 'column', 'tables': []}, - {'type': 'function', 'schema': []}, - {'type': 'keyword'}, - ]) + {'type': 'column', 'tables': []}, + {'type': 'function', 'schema': []}, + {'type': 'keyword'}, + ]) + @pytest.mark.parametrize('expression', [ 'SELECT * FROM ', @@ -102,6 +114,7 @@ def test_expression_suggests_tables_views_and_schemas(expression): {'type': 'view', 'schema': []}, {'type': 'schema'}]) + @pytest.mark.parametrize('expression', [ 'SELECT * FROM sch.', 'INSERT INTO sch.', @@ -118,37 +131,43 @@ def test_expression_suggests_qualified_tables_views_and_schemas(expression): {'type': 'table', 'schema': 'sch'}, {'type': 'view', 'schema': 'sch'}]) + def test_truncate_suggests_tables_and_schemas(): suggestions = suggest_type('TRUNCATE ', 'TRUNCATE ') assert sorted_dicts(suggestions) == sorted_dicts([ {'type': 'table', 'schema': []}, {'type': 'schema'}]) + def test_truncate_suggests_qualified_tables(): suggestions = suggest_type('TRUNCATE sch.', 'TRUNCATE sch.') assert sorted_dicts(suggestions) == sorted_dicts([ {'type': 'table', 'schema': 'sch'}]) + def test_distinct_suggests_cols(): suggestions = suggest_type('SELECT DISTINCT ', 'SELECT DISTINCT ') assert suggestions == [{'type': 'column', 'tables': []}] + def test_col_comma_suggests_cols(): suggestions = suggest_type('SELECT a, b, FROM tbl', 'SELECT a, b,') assert sorted_dicts(suggestions) == sorted_dicts([ {'type': 'column', 'tables': [(None, 'tbl', None)]}, {'type': 'function', 'schema': []}, {'type': 'keyword'}, - ]) + ]) + def test_table_comma_suggests_tables_and_schemas(): suggestions = suggest_type('SELECT a, b FROM tbl1, ', - 'SELECT a, b FROM tbl1, ') + 'SELECT a, b FROM tbl1, ') assert sorted_dicts(suggestions) == sorted_dicts([ {'type': 'table', 'schema': []}, {'type': 'view', 'schema': []}, {'type': 'schema'}]) + def test_into_suggests_tables_and_schemas(): suggestion = suggest_type('INSERT INTO ', 'INSERT INTO ') assert sorted_dicts(suggestion) == sorted_dicts([ @@ -156,26 +175,31 @@ def test_into_suggests_tables_and_schemas(): {'type': 'view', 'schema': []}, {'type': 'schema'}]) + def test_insert_into_lparen_suggests_cols(): suggestions = suggest_type('INSERT INTO abc (', 'INSERT INTO abc (') assert suggestions == [{'type': 'column', 'tables': [(None, 'abc', None)]}] + def test_insert_into_lparen_partial_text_suggests_cols(): suggestions = suggest_type('INSERT INTO abc (i', 'INSERT INTO abc (i') assert suggestions == [{'type': 'column', 'tables': [(None, 'abc', None)]}] + def test_insert_into_lparen_comma_suggests_cols(): suggestions = suggest_type('INSERT INTO abc (id,', 'INSERT INTO abc (id,') assert suggestions == [{'type': 'column', 'tables': [(None, 'abc', None)]}] + def test_partially_typed_col_name_suggests_col_names(): suggestions = suggest_type('SELECT * FROM tabl WHERE col_n', - 'SELECT * FROM tabl WHERE col_n') + 'SELECT * FROM tabl WHERE col_n') assert sorted_dicts(suggestions) == sorted_dicts([ {'type': 'column', 'tables': [(None, 'tabl', None)]}, {'type': 'function', 'schema': []}, {'type': 'keyword'}, - ]) + ]) + def test_dot_suggests_cols_of_a_table_or_schema_qualified_table(): suggestions = suggest_type('SELECT tabl. FROM tabl', 'SELECT tabl.') @@ -185,24 +209,27 @@ def test_dot_suggests_cols_of_a_table_or_schema_qualified_table(): {'type': 'view', 'schema': 'tabl'}, {'type': 'function', 'schema': 'tabl'}]) + def test_dot_suggests_cols_of_an_alias(): suggestions = suggest_type('SELECT t1. FROM tabl1 t1, tabl2 t2', - 'SELECT t1.') + 'SELECT t1.') assert sorted_dicts(suggestions) == sorted_dicts([ {'type': 'table', 'schema': 't1'}, {'type': 'view', 'schema': 't1'}, {'type': 'column', 'tables': [(None, 'tabl1', 't1')]}, {'type': 'function', 'schema': 't1'}]) + def test_dot_col_comma_suggests_cols_or_schema_qualified_table(): suggestions = suggest_type('SELECT t1.a, t2. FROM tabl1 t1, tabl2 t2', - 'SELECT t1.a, t2.') + 'SELECT t1.a, t2.') assert sorted_dicts(suggestions) == sorted_dicts([ {'type': 'column', 'tables': [(None, 'tabl2', 't2')]}, {'type': 'table', 'schema': 't2'}, {'type': 'view', 'schema': 't2'}, {'type': 'function', 'schema': 't2'}]) + @pytest.mark.parametrize('expression', [ 'SELECT * FROM (', 'SELECT * FROM foo WHERE EXISTS (', @@ -212,6 +239,7 @@ def test_sub_select_suggests_keyword(expression): suggestion = suggest_type(expression, expression) assert suggestion == [{'type': 'keyword'}] + @pytest.mark.parametrize('expression', [ 'SELECT * FROM (S', 'SELECT * FROM foo WHERE EXISTS (S', @@ -221,6 +249,7 @@ def test_sub_select_partial_text_suggests_keyword(expression): suggestion = suggest_type(expression, expression) assert suggestion == [{'type': 'keyword'}] + def test_outer_table_reference_in_exists_subquery_suggests_columns(): q = 'SELECT * FROM foo f WHERE EXISTS (SELECT 1 FROM bar WHERE f.' suggestions = suggest_type(q, q) @@ -230,6 +259,7 @@ def test_outer_table_reference_in_exists_subquery_suggests_columns(): {'type': 'view', 'schema': 'f'}, {'type': 'function', 'schema': 'f'}] + @pytest.mark.parametrize('expression', [ 'SELECT * FROM (SELECT * FROM ', 'SELECT * FROM foo WHERE EXISTS (SELECT * FROM ', @@ -242,32 +272,36 @@ def test_sub_select_table_name_completion(expression): {'type': 'view', 'schema': []}, {'type': 'schema'}]) + def test_sub_select_col_name_completion(): suggestions = suggest_type('SELECT * FROM (SELECT FROM abc', - 'SELECT * FROM (SELECT ') + 'SELECT * FROM (SELECT ') assert sorted_dicts(suggestions) == sorted_dicts([ {'type': 'column', 'tables': [(None, 'abc', None)]}, {'type': 'function', 'schema': []}, {'type': 'keyword'}, - ]) + ]) + @pytest.mark.xfail def test_sub_select_multiple_col_name_completion(): suggestions = suggest_type('SELECT * FROM (SELECT a, FROM abc', - 'SELECT * FROM (SELECT a, ') + 'SELECT * FROM (SELECT a, ') assert sorted_dicts(suggestions) == sorted_dicts([ {'type': 'column', 'tables': [(None, 'abc', None)]}, {'type': 'function', 'schema': []}]) + def test_sub_select_dot_col_name_completion(): suggestions = suggest_type('SELECT * FROM (SELECT t. FROM tabl t', - 'SELECT * FROM (SELECT t.') + 'SELECT * FROM (SELECT t.') assert sorted_dicts(suggestions) == sorted_dicts([ {'type': 'column', 'tables': [(None, 'tabl', 't')]}, {'type': 'table', 'schema': 't'}, {'type': 'view', 'schema': 't'}, {'type': 'function', 'schema': 't'}]) + @pytest.mark.parametrize('join_type', ['', 'INNER', 'LEFT', 'RIGHT OUTER']) @pytest.mark.parametrize('tbl_alias', ['', 'foo']) def test_join_suggests_tables_and_schemas(tbl_alias, join_type): @@ -278,6 +312,7 @@ def test_join_suggests_tables_and_schemas(tbl_alias, join_type): {'type': 'view', 'schema': []}, {'type': 'schema'}]) + @pytest.mark.parametrize('sql', [ 'SELECT * FROM abc a JOIN def d ON a.', 'SELECT * FROM abc a JOIN def d ON a.id = d.id AND a.', @@ -290,6 +325,7 @@ def test_join_alias_dot_suggests_cols1(sql): {'type': 'view', 'schema': 'a'}, {'type': 'function', 'schema': 'a'}]) + @pytest.mark.parametrize('sql', [ 'SELECT * FROM abc a JOIN def d ON a.id = d.', 'SELECT * FROM abc a JOIN def d ON a.id = d.id AND a.id2 = d.', @@ -302,6 +338,7 @@ def test_join_alias_dot_suggests_cols2(sql): {'type': 'view', 'schema': 'd'}, {'type': 'function', 'schema': 'd'}]) + @pytest.mark.parametrize('sql', [ 'select a.x, b.y from abc a join bcd b on ', 'select a.x, b.y from abc a join bcd b on a.id = b.id OR ', @@ -310,6 +347,7 @@ def test_on_suggests_aliases(sql): suggestions = suggest_type(sql, sql) assert suggestions == [{'type': 'alias', 'aliases': ['a', 'b']}] + @pytest.mark.parametrize('sql', [ 'select abc.x, bcd.y from abc join bcd on ', 'select abc.x, bcd.y from abc join bcd on abc.id = bcd.id AND ', @@ -318,6 +356,7 @@ def test_on_suggests_tables(sql): suggestions = suggest_type(sql, sql) assert suggestions == [{'type': 'alias', 'aliases': ['abc', 'bcd']}] + @pytest.mark.parametrize('sql', [ 'select a.x, b.y from abc a join bcd b on a.id = ', 'select a.x, b.y from abc a join bcd b on a.id = b.id AND a.id2 = ', @@ -326,6 +365,7 @@ def test_on_suggests_aliases_right_side(sql): suggestions = suggest_type(sql, sql) assert suggestions == [{'type': 'alias', 'aliases': ['a', 'b']}] + @pytest.mark.parametrize('sql', [ 'select abc.x, bcd.y from abc join bcd on ', 'select abc.x, bcd.y from abc join bcd on abc.id = bcd.id and ', @@ -348,57 +388,59 @@ def test_2_statements_2nd_current(): suggestions = suggest_type('select * from a; select * from ', 'select * from a; select * from ') assert sorted_dicts(suggestions) == sorted_dicts([ - {'type': 'table', 'schema': []}, - {'type': 'view', 'schema': []}, - {'type': 'schema'}]) + {'type': 'table', 'schema': []}, + {'type': 'view', 'schema': []}, + {'type': 'schema'}]) suggestions = suggest_type('select * from a; select from b', 'select * from a; select ') assert sorted_dicts(suggestions) == sorted_dicts([ - {'type': 'column', 'tables': [(None, 'b', None)]}, - {'type': 'function', 'schema': []}, - {'type': 'keyword'}, - ]) + {'type': 'column', 'tables': [(None, 'b', None)]}, + {'type': 'function', 'schema': []}, + {'type': 'keyword'}, + ]) # Should work even if first statement is invalid suggestions = suggest_type('select * from; select * from ', 'select * from; select * from ') assert sorted_dicts(suggestions) == sorted_dicts([ - {'type': 'table', 'schema': []}, - {'type': 'view', 'schema': []}, - {'type': 'schema'}]) + {'type': 'table', 'schema': []}, + {'type': 'view', 'schema': []}, + {'type': 'schema'}]) + def test_2_statements_1st_current(): suggestions = suggest_type('select * from ; select * from b', 'select * from ') assert sorted_dicts(suggestions) == sorted_dicts([ - {'type': 'table', 'schema': []}, - {'type': 'view', 'schema': []}, - {'type': 'schema'}]) + {'type': 'table', 'schema': []}, + {'type': 'view', 'schema': []}, + {'type': 'schema'}]) suggestions = suggest_type('select from a; select * from b', 'select ') assert sorted_dicts(suggestions) == sorted_dicts([ - {'type': 'column', 'tables': [(None, 'a', None)]}, - {'type': 'function', 'schema': []}, - {'type': 'keyword'}, - ]) + {'type': 'column', 'tables': [(None, 'a', None)]}, + {'type': 'function', 'schema': []}, + {'type': 'keyword'}, + ]) + def test_3_statements_2nd_current(): suggestions = suggest_type('select * from a; select * from ; select * from c', 'select * from a; select * from ') assert sorted_dicts(suggestions) == sorted_dicts([ - {'type': 'table', 'schema': []}, - {'type': 'view', 'schema': []}, - {'type': 'schema'}]) + {'type': 'table', 'schema': []}, + {'type': 'view', 'schema': []}, + {'type': 'schema'}]) suggestions = suggest_type('select * from a; select from b; select * from c', 'select * from a; select ') assert sorted_dicts(suggestions) == sorted_dicts([ - {'type': 'column', 'tables': [(None, 'b', None)]}, - {'type': 'function', 'schema': []}, - {'type': 'keyword'}, - ]) + {'type': 'column', 'tables': [(None, 'b', None)]}, + {'type': 'function', 'schema': []}, + {'type': 'keyword'}, + ]) def test_create_db_with_template(): @@ -435,13 +477,15 @@ def test_handle_pre_completion_comma_gracefully(text): assert iter(suggestions) + def test_cross_join(): text = 'select * from v1 cross join v2 JOIN v1.id, ' suggestions = suggest_type(text, text) assert sorted_dicts(suggestions) == sorted_dicts([ - {'type': 'table', 'schema': []}, - {'type': 'view', 'schema': []}, - {'type': 'schema'}]) + {'type': 'table', 'schema': []}, + {'type': 'view', 'schema': []}, + {'type': 'schema'}]) + @pytest.mark.parametrize('expression', [ 'SELECT 1 AS ', diff --git a/test/test_completion_refresher.py b/test/test_completion_refresher.py index 8851eae6..1ed63774 100644 --- a/test/test_completion_refresher.py +++ b/test/test_completion_refresher.py @@ -10,10 +10,11 @@ def refresher(): def test_ctor(refresher): - """ - Refresher object should contain a few handlers + """Refresher object should contain a few handlers. + :param refresher: :return: + """ assert len(refresher.refreshers) > 0 actual_handlers = list(refresher.refreshers.keys()) @@ -41,10 +42,11 @@ def test_refresh_called_once(refresher): def test_refresh_called_twice(refresher): - """ - If refresh is called a second time, it should be restarted + """If refresh is called a second time, it should be restarted. + :param refresher: :return: + """ callbacks = Mock() @@ -69,9 +71,10 @@ def dummy_bg_refresh(*args): def test_refresh_with_callbacks(refresher): - """ - Callbacks must be called + """Callbacks must be called. + :param refresher: + """ callbacks = [Mock()] sqlexecute_class = Mock() diff --git a/test/test_config.py b/test/test_config.py index 2a0d26c1..8ef8b781 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -11,7 +11,7 @@ read_and_decrypt_mylogin_cnf, str_to_bool) with_pycryptodome = ['pycryptodome' in set([package.project_name for package in - pip.get_installed_distributions()])] + pip.get_installed_distributions()])] LOGIN_PATH_FILE = os.path.abspath(os.path.join(os.path.dirname(__file__), 'mylogin.cnf')) diff --git a/test/test_dbspecial.py b/test/test_dbspecial.py index 17309b29..3733c813 100644 --- a/test/test_dbspecial.py +++ b/test/test_dbspecial.py @@ -2,10 +2,11 @@ from test_completion_engine import sorted_dicts from mycli.packages.special.utils import format_uptime + def test_u_suggests_databases(): suggestions = suggest_type('\\u ', '\\u ') assert sorted_dicts(suggestions) == sorted_dicts([ - {'type': 'database'}]) + {'type': 'database'}]) def test_describe_table(): diff --git a/test/test_main.py b/test/test_main.py index 1271f18d..eb9c8623 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -18,6 +18,7 @@ CLI_ARGS = ['--user', USER, '--host', HOST, '--port', PORT, '--password', PASSWORD, '_test_db'] + @dbtest def test_execute_arg(executor): run(executor, 'create table test (a text)') @@ -84,6 +85,7 @@ def test_batch_mode(executor): assert result.exit_code == 0 assert 'count(*)\n3\n\na\nabc\n' in result.output + @dbtest def test_batch_mode_table(executor): run(executor, '''create table test(a text)''') @@ -112,6 +114,7 @@ def test_batch_mode_table(executor): assert result.exit_code == 0 assert expected in result.output + @dbtest def test_batch_mode_csv(executor): run(executor, '''create table test(a text, b text)''') @@ -127,6 +130,7 @@ def test_batch_mode_csv(executor): assert result.exit_code == 0 assert expected in result.output + def test_query_starts_with(executor): query = 'USE test;' assert query_starts_with(query, ('use', )) is True @@ -134,10 +138,12 @@ def test_query_starts_with(executor): query = 'DROP DATABASE test;' assert query_starts_with(query, ('use', )) is False + def test_query_starts_with_comment(executor): query = '# comment\nUSE test;' assert query_starts_with(query, ('use', )) is True + def test_queries_start_with(executor): sql = ( '# comment\n' @@ -148,6 +154,7 @@ def test_queries_start_with(executor): assert queries_start_with(sql, ('use', 'drop')) is True assert queries_start_with(sql, ('delete', 'update')) is False + def test_is_destructive(executor): sql = ( 'use test;\n' @@ -156,6 +163,7 @@ def test_is_destructive(executor): ) assert is_destructive(sql) is True + def test_confirm_destructive_query_notty(executor): stdin = click.get_text_stream('stdin') assert stdin.isatty() is False @@ -163,6 +171,7 @@ def test_confirm_destructive_query_notty(executor): sql = 'drop database foo;' assert confirm_destructive_query(sql) is None + def test_thanks_picker_utf8(): project_root = os.path.dirname(PACKAGE_ROOT) author_file = os.path.join(project_root, 'AUTHORS') diff --git a/test/test_naive_completion.py b/test/test_naive_completion.py index 57d738bc..3282c7ed 100644 --- a/test/test_naive_completion.py +++ b/test/test_naive_completion.py @@ -3,16 +3,19 @@ from prompt_toolkit.completion import Completion from prompt_toolkit.document import Document + @pytest.fixture def completer(): import mycli.sqlcompleter as sqlcompleter return sqlcompleter.SQLCompleter(smart_completion=False) + @pytest.fixture def complete_event(): from mock import Mock return Mock() + def test_empty_string_completion(completer, complete_event): text = '' position = 0 @@ -21,6 +24,7 @@ def test_empty_string_completion(completer, complete_event): complete_event)) assert result == set(map(Completion, completer.all_completions)) + def test_select_keyword_completion(completer, complete_event): text = 'SEL' position = len('SEL') @@ -29,6 +33,7 @@ def test_select_keyword_completion(completer, complete_event): complete_event)) assert result == set([Completion(text='SELECT', start_position=-3)]) + def test_function_name_completion(completer, complete_event): text = 'SELECT MA' position = len('SELECT MA') @@ -39,6 +44,7 @@ def test_function_name_completion(completer, complete_event): Completion(text='MAX', start_position=-2), Completion(text='MASTER', start_position=-2)]) + def test_column_name_completion(completer, complete_event): text = 'SELECT FROM users' position = len('SELECT ') diff --git a/test/test_parseutils.py b/test/test_parseutils.py index e512632c..e45cab78 100644 --- a/test/test_parseutils.py +++ b/test/test_parseutils.py @@ -6,50 +6,62 @@ def test_empty_string(): tables = extract_tables('') assert tables == [] + def test_simple_select_single_table(): tables = extract_tables('select * from abc') assert tables == [(None, 'abc', None)] + def test_simple_select_single_table_schema_qualified(): tables = extract_tables('select * from abc.def') assert tables == [('abc', 'def', None)] + def test_simple_select_multiple_tables(): tables = extract_tables('select * from abc, def') assert sorted(tables) == [(None, 'abc', None), (None, 'def', None)] + def test_simple_select_multiple_tables_schema_qualified(): tables = extract_tables('select * from abc.def, ghi.jkl') assert sorted(tables) == [('abc', 'def', None), ('ghi', 'jkl', None)] + def test_simple_select_with_cols_single_table(): tables = extract_tables('select a,b from abc') assert tables == [(None, 'abc', None)] + def test_simple_select_with_cols_single_table_schema_qualified(): tables = extract_tables('select a,b from abc.def') assert tables == [('abc', 'def', None)] + def test_simple_select_with_cols_multiple_tables(): tables = extract_tables('select a,b from abc, def') assert sorted(tables) == [(None, 'abc', None), (None, 'def', None)] + def test_simple_select_with_cols_multiple_tables_with_schema(): tables = extract_tables('select a,b from abc.def, def.ghi') assert sorted(tables) == [('abc', 'def', None), ('def', 'ghi', None)] + def test_select_with_hanging_comma_single_table(): tables = extract_tables('select a, from abc') assert tables == [(None, 'abc', None)] + def test_select_with_hanging_comma_multiple_tables(): tables = extract_tables('select a, from abc, def') assert sorted(tables) == [(None, 'abc', None), (None, 'def', None)] + def test_select_with_hanging_period_multiple_tables(): tables = extract_tables('SELECT t1. FROM tabl1 t1, tabl2 t2') assert sorted(tables) == [(None, 'tabl1', 't1'), (None, 'tabl2', 't2')] + def test_simple_insert_single_table(): tables = extract_tables('insert into abc (id, name) values (1, "def")') @@ -57,27 +69,34 @@ def test_simple_insert_single_table(): # assert tables == [(None, 'abc', None)] assert tables == [(None, 'abc', 'abc')] + @pytest.mark.xfail def test_simple_insert_single_table_schema_qualified(): tables = extract_tables('insert into abc.def (id, name) values (1, "def")') assert tables == [('abc', 'def', None)] + def test_simple_update_table(): tables = extract_tables('update abc set id = 1') assert tables == [(None, 'abc', None)] + def test_simple_update_table_with_schema(): tables = extract_tables('update abc.def set id = 1') assert tables == [('abc', 'def', None)] + def test_join_table(): tables = extract_tables('SELECT * FROM abc a JOIN def d ON a.id = d.num') assert sorted(tables) == [(None, 'abc', 'a'), (None, 'def', 'd')] + def test_join_table_schema_qualified(): - tables = extract_tables('SELECT * FROM abc.def x JOIN ghi.jkl y ON x.id = y.num') + tables = extract_tables( + 'SELECT * FROM abc.def x JOIN ghi.jkl y ON x.id = y.num') assert tables == [('abc', 'def', 'x'), ('ghi', 'jkl', 'y')] + def test_join_as_table(): tables = extract_tables('SELECT * FROM my_table AS m WHERE m.a > 5') assert tables == [(None, 'my_table', 'm')] diff --git a/test/test_smart_completion_public_schema_only.py b/test/test_smart_completion_public_schema_only.py index e99567a0..6cdc9c30 100644 --- a/test/test_smart_completion_public_schema_only.py +++ b/test/test_smart_completion_public_schema_only.py @@ -5,11 +5,12 @@ from prompt_toolkit.document import Document metadata = { - 'users': ['id', 'email', 'first_name', 'last_name'], - 'orders': ['id', 'ordered_date', 'status'], - 'select': ['id', 'insert', 'ABC'], - 'réveillé': ['id', 'insert', 'ABC'] - } + 'users': ['id', 'email', 'first_name', 'last_name'], + 'orders': ['id', 'ordered_date', 'status'], + 'select': ['id', 'insert', 'ABC'], + 'réveillé': ['id', 'insert', 'ABC'] +} + @pytest.fixture def completer(): @@ -30,11 +31,13 @@ def completer(): return comp + @pytest.fixture def complete_event(): from mock import Mock return Mock() + def test_empty_string_completion(completer, complete_event): text = '' position = 0 @@ -44,6 +47,7 @@ def test_empty_string_completion(completer, complete_event): complete_event)) assert set(map(Completion, completer.keywords)) == result + def test_select_keyword_completion(completer, complete_event): text = 'SEL' position = len('SEL') @@ -73,12 +77,14 @@ def test_function_name_completion(completer, complete_event): Completion(text='MASTER', start_position=-2), ]) + def test_suggested_column_names(completer, complete_event): - """ - Suggest column and function names when selecting from table + """Suggest column and function names when selecting from table. + :param completer: :param complete_event: :return: + """ text = 'SELECT from users' position = len('SELECT ') @@ -94,13 +100,15 @@ def test_suggested_column_names(completer, complete_event): list(map(Completion, completer.functions)) + list(map(Completion, completer.keywords))) + def test_suggested_column_names_in_function(completer, complete_event): - """ - Suggest column and function names when selecting multiple - columns from table + """Suggest column and function names when selecting multiple columns from + table. + :param completer: :param complete_event: :return: + """ text = 'SELECT MAX( from users' position = len('SELECT MAX(') @@ -114,12 +122,14 @@ def test_suggested_column_names_in_function(completer, complete_event): Completion(text='first_name', start_position=0), Completion(text='last_name', start_position=0)]) + def test_suggested_column_names_with_table_dot(completer, complete_event): - """ - Suggest column names on table name and dot + """Suggest column names on table name and dot. + :param completer: :param complete_event: :return: + """ text = 'SELECT users. from users' position = len('SELECT users.') @@ -133,12 +143,14 @@ def test_suggested_column_names_with_table_dot(completer, complete_event): Completion(text='first_name', start_position=0), Completion(text='last_name', start_position=0)]) + def test_suggested_column_names_with_alias(completer, complete_event): - """ - Suggest column names on table alias and dot + """Suggest column names on table alias and dot. + :param completer: :param complete_event: :return: + """ text = 'SELECT u. from users u' position = len('SELECT u.') @@ -152,13 +164,15 @@ def test_suggested_column_names_with_alias(completer, complete_event): Completion(text='first_name', start_position=0), Completion(text='last_name', start_position=0)]) + def test_suggested_multiple_column_names(completer, complete_event): - """ - Suggest column and function names when selecting multiple - columns from table + """Suggest column and function names when selecting multiple columns from + table. + :param completer: :param complete_event: :return: + """ text = 'SELECT id, from users u' position = len('SELECT id, ') @@ -174,13 +188,15 @@ def test_suggested_multiple_column_names(completer, complete_event): list(map(Completion, completer.functions)) + list(map(Completion, completer.keywords))) + def test_suggested_multiple_column_names_with_alias(completer, complete_event): - """ - Suggest column names on table alias and dot - when selecting multiple columns from table + """Suggest column names on table alias and dot when selecting multiple + columns from table. + :param completer: :param complete_event: :return: + """ text = 'SELECT u.id, u. from users u' position = len('SELECT u.id, u.') @@ -194,13 +210,15 @@ def test_suggested_multiple_column_names_with_alias(completer, complete_event): Completion(text='first_name', start_position=0), Completion(text='last_name', start_position=0)]) + def test_suggested_multiple_column_names_with_dot(completer, complete_event): - """ - Suggest column names on table names and dot - when selecting multiple columns from table + """Suggest column names on table names and dot when selecting multiple + columns from table. + :param completer: :param complete_event: :return: + """ text = 'SELECT users.id, users. from users u' position = len('SELECT users.id, users.') @@ -214,6 +232,7 @@ def test_suggested_multiple_column_names_with_dot(completer, complete_event): Completion(text='first_name', start_position=0), Completion(text='last_name', start_position=0)]) + def test_suggested_aliases_after_on(completer, complete_event): text = 'SELECT u.name, o.id FROM users u JOIN orders o ON ' position = len('SELECT u.name, o.id FROM users u JOIN orders o ON ') @@ -224,9 +243,11 @@ def test_suggested_aliases_after_on(completer, complete_event): Completion(text='u', start_position=0), Completion(text='o', start_position=0)]) + def test_suggested_aliases_after_on_right_side(completer, complete_event): text = 'SELECT u.name, o.id FROM users u JOIN orders o ON o.user_id = ' - position = len('SELECT u.name, o.id FROM users u JOIN orders o ON o.user_id = ') + position = len( + 'SELECT u.name, o.id FROM users u JOIN orders o ON o.user_id = ') result = set(completer.get_completions( Document(text=text, cursor_position=position), complete_event)) @@ -234,6 +255,7 @@ def test_suggested_aliases_after_on_right_side(completer, complete_event): Completion(text='u', start_position=0), Completion(text='o', start_position=0)]) + def test_suggested_tables_after_on(completer, complete_event): text = 'SELECT users.name, orders.id FROM users JOIN orders ON ' position = len('SELECT users.name, orders.id FROM users JOIN orders ON ') @@ -244,9 +266,11 @@ def test_suggested_tables_after_on(completer, complete_event): Completion(text='users', start_position=0), Completion(text='orders', start_position=0)]) + def test_suggested_tables_after_on_right_side(completer, complete_event): text = 'SELECT users.name, orders.id FROM users JOIN orders ON orders.user_id = ' - position = len('SELECT users.name, orders.id FROM users JOIN orders ON orders.user_id = ') + position = len( + 'SELECT users.name, orders.id FROM users JOIN orders ON orders.user_id = ') result = set(completer.get_completions( Document(text=text, cursor_position=position), complete_event)) @@ -254,6 +278,7 @@ def test_suggested_tables_after_on_right_side(completer, complete_event): Completion(text='users', start_position=0), Completion(text='orders', start_position=0)]) + def test_table_names_after_from(completer, complete_event): text = 'SELECT * FROM ' position = len('SELECT * FROM ') @@ -265,7 +290,8 @@ def test_table_names_after_from(completer, complete_event): Completion(text='orders', start_position=0), Completion(text='`réveillé`', start_position=0), Completion(text='`select`', start_position=0), - ]) + ]) + def test_auto_escaped_col_names(completer, complete_event): text = 'SELECT from `select`' @@ -281,6 +307,7 @@ def test_auto_escaped_col_names(completer, complete_event): list(map(Completion, completer.functions)) + list(map(Completion, completer.keywords))) + def test_un_escaped_table_names(completer, complete_event): text = 'SELECT from réveillé' position = len('SELECT ') diff --git a/test/test_special_iocommands.py b/test/test_special_iocommands.py index 2ed2dbad..708c7fd7 100644 --- a/test/test_special_iocommands.py +++ b/test/test_special_iocommands.py @@ -23,18 +23,21 @@ def test_set_get_pager(): mycli.packages.special.disable_pager() assert not mycli.packages.special.is_pager_enabled() + def test_set_get_timing(): mycli.packages.special.set_timing_enabled(True) assert mycli.packages.special.is_timing_enabled() mycli.packages.special.set_timing_enabled(False) assert not mycli.packages.special.is_timing_enabled() + def test_set_get_expanded_output(): mycli.packages.special.set_expanded_output(True) assert mycli.packages.special.is_expanded_output() mycli.packages.special.set_expanded_output(False) assert not mycli.packages.special.is_expanded_output() + def test_editor_command(): assert mycli.packages.special.editor_command(r'hello\e') assert mycli.packages.special.editor_command(r'\ehello') @@ -45,14 +48,15 @@ def test_editor_command(): os.environ['EDITOR'] = 'true' mycli.packages.special.open_external_editor(r'select 1') == "select 1" + def test_tee_command(): - mycli.packages.special.write_tee(u"hello world") # write without file set + mycli.packages.special.write_tee(u"hello world") # write without file set with tempfile.NamedTemporaryFile() as f: - mycli.packages.special.execute(None, u"tee "+f.name) + mycli.packages.special.execute(None, u"tee " + f.name) mycli.packages.special.write_tee(u"hello world") assert f.read() == b"hello world\n" - mycli.packages.special.execute(None, u"tee -o "+f.name) + mycli.packages.special.execute(None, u"tee -o " + f.name) mycli.packages.special.write_tee(u"hello world") f.seek(0) assert f.read() == b"hello world\n" @@ -62,6 +66,7 @@ def test_tee_command(): f.seek(0) assert f.read() == b"hello world\n" + def test_tee_command_error(): with pytest.raises(TypeError): mycli.packages.special.execute(None, 'tee') @@ -71,8 +76,10 @@ def test_tee_command_error(): os.chmod(f.name, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) mycli.packages.special.execute(None, 'tee {}'.format(f.name)) + def test_favorite_query(): with utils.db_connection().cursor() as cur: query = u'select "✔"' mycli.packages.special.execute(cur, u'\\fs check {0}'.format(query)) - assert next(mycli.packages.special.execute(cur, u'\\f check'))[0] == "> " + query + assert next(mycli.packages.special.execute( + cur, u'\\f check'))[0] == "> " + query diff --git a/test/test_sqlexecute.py b/test/test_sqlexecute.py index 157b6234..aebfa1a6 100644 --- a/test/test_sqlexecute.py +++ b/test/test_sqlexecute.py @@ -20,6 +20,7 @@ def test_conn(executor): +-----+ 1 row in set""") + @dbtest def test_bools(executor): run(executor, '''create table test(a boolean)''') @@ -33,6 +34,7 @@ def test_bools(executor): +---+ 1 row in set""") + @dbtest def test_binary(executor): run(executor, '''create table bt(geom linestring NOT NULL)''') @@ -46,6 +48,7 @@ def test_binary(executor): +----------------------------------------------------------------------------------------------+ 1 row in set""") + @dbtest def test_binary_expanded(executor): run(executor, '''create table bt(geom linestring NOT NULL)''') @@ -57,6 +60,7 @@ def test_binary_expanded(executor): 1 row in set""") + @dbtest def test_table_and_columns_query(executor): run(executor, "create table a(x text, y text)") @@ -64,25 +68,29 @@ def test_table_and_columns_query(executor): assert set(executor.tables()) == set([('a',), ('b',)]) assert set(executor.table_columns()) == set( - [('a', 'x'), ('a', 'y'), ('b', 'z')]) + [('a', 'x'), ('a', 'y'), ('b', 'z')]) + @dbtest def test_database_list(executor): databases = executor.databases() assert '_test_db' in databases + @dbtest def test_invalid_syntax(executor): with pytest.raises(pymysql.ProgrammingError) as excinfo: run(executor, 'invalid syntax!') assert 'You have an error in your SQL syntax;' in str(excinfo.value) + @dbtest def test_invalid_column_name(executor): with pytest.raises(pymysql.InternalError) as excinfo: run(executor, 'select invalid command') assert "Unknown column 'invalid' in 'field list'" in str(excinfo.value) + @dbtest def test_unicode_support_in_output(executor): run(executor, "create table unicodechars(t text)") @@ -91,6 +99,7 @@ def test_unicode_support_in_output(executor): # See issue #24, this raises an exception without proper handling assert u'é' in run(executor, u"select * from unicodechars", join=True) + @dbtest def test_expanded_output(executor): run(executor, '''create table test(a text)''') @@ -112,19 +121,23 @@ def test_expanded_output(executor): assert results in expected_results + @dbtest def test_multiple_queries_same_line(executor): result = run(executor, "select 'foo'; select 'bar'") - assert len(result) == 4 # 2 for the results and 2 more for status messages. + # 2 for the results and 2 more for status messages. + assert len(result) == 4 assert "foo" in result[0] assert "bar" in result[2] + @dbtest def test_multiple_queries_same_line_syntaxerror(executor): with pytest.raises(pymysql.ProgrammingError) as excinfo: run(executor, "select 'foo'; invalid syntax") assert 'You have an error in your SQL syntax;' in str(excinfo.value) + @dbtest def test_favorite_query(executor): set_expanded_output(False) @@ -147,6 +160,7 @@ def test_favorite_query(executor): results = run(executor, "\\fd test-a") assert results == ['test-a: Deleted'] + @dbtest def test_favorite_query_multiple_statement(executor): set_expanded_output(False) @@ -176,6 +190,7 @@ def test_favorite_query_multiple_statement(executor): results = run(executor, "\\fd test-ad") assert results == ['test-ad: Deleted'] + @dbtest def test_favorite_query_expanded_output(executor): set_expanded_output(False) @@ -206,6 +221,7 @@ def test_favorite_query_expanded_output(executor): results = run(executor, "\\fd test-ae") assert results == ['test-ae: Deleted'] + @dbtest def test_special_command(executor): results = run(executor, '\\?') @@ -213,6 +229,7 @@ def test_special_command(executor): assert len(results) == 1 assert expected_line in results[0] + @dbtest def test_cd_command_without_a_folder_name(executor): results = run(executor, 'system cd') @@ -220,6 +237,7 @@ def test_cd_command_without_a_folder_name(executor): assert len(results) == 1 assert expected_line in results[0] + @dbtest def test_system_command_not_found(executor): results = run(executor, 'system xyz') @@ -227,6 +245,7 @@ def test_system_command_not_found(executor): expected_line = 'OSError:' assert expected_line in results[0] + @dbtest def test_system_command_output(executor): test_file_path = os.path.join(os.path.abspath('.'), 'test', 'test.txt') @@ -235,16 +254,19 @@ def test_system_command_output(executor): expected_line = u'mycli rocks!\n' assert expected_line == results[0] + @dbtest def test_cd_command_current_dir(executor): test_path = os.path.join(os.path.abspath('.'), 'test') results = run(executor, 'system cd {0}'.format(test_path)) assert os.getcwd() == test_path + @dbtest def test_unicode_support(executor): assert u'日本語' in run(executor, u"SELECT '日本語' AS japanese;", join=True) + @dbtest def test_favorite_query_multiline_statement(executor): set_expanded_output(False) @@ -274,6 +296,7 @@ def test_favorite_query_multiline_statement(executor): results = run(executor, "\\fd test-ad") assert results == ['test-ad: Deleted'] + @dbtest def test_timestamp_null(executor): run(executor, '''create table ts_null(a timestamp)''') @@ -287,6 +310,7 @@ def test_timestamp_null(executor): +---------------------+ 1 row in set""") + @dbtest def test_datetime_null(executor): run(executor, '''create table dt_null(a datetime)''') @@ -300,6 +324,7 @@ def test_datetime_null(executor): +---------------------+ 1 row in set""") + @dbtest def test_date_null(executor): run(executor, '''create table date_null(a date)''') @@ -313,6 +338,7 @@ def test_date_null(executor): +------------+ 1 row in set""") + @dbtest def test_time_null(executor): run(executor, '''create table time_null(a time)''') diff --git a/test/utils.py b/test/utils.py index b29e5e07..0d6b6a99 100644 --- a/test/utils.py +++ b/test/utils.py @@ -11,13 +11,15 @@ PORT = getenv('PYTEST_PORT', 3306) CHARSET = getenv('PYTEST_CHARSET', 'utf8') + def db_connection(dbname=None): conn = pymysql.connect(user=USER, host=HOST, port=PORT, database=dbname, password=PASSWORD, - charset=CHARSET, - local_infile=False) + charset=CHARSET, + local_infile=False) conn.autocommit = True return conn + try: db_connection() CAN_CONNECT_TO_DB = True @@ -28,6 +30,7 @@ def db_connection(dbname=None): not CAN_CONNECT_TO_DB, reason="Need a mysql instance at localhost accessible by user 'root'") + def create_db(dbname): with db_connection().cursor() as cur: try: @@ -36,8 +39,9 @@ def create_db(dbname): except: pass + def run(executor, sql, join=False): - " Return string output for the sql to be run " + """Return string output for the sql to be run.""" result = [] # TODO: this needs to go away. `run()` should not test formatted output. @@ -51,6 +55,7 @@ def run(executor, sql, join=False): result = '\n'.join(result) return result + def set_expanded_output(is_expanded): - """ Pass-through for the tests """ + """Pass-through for the tests.""" return special.set_expanded_output(is_expanded) From 072f343989c33352536ee52293e58ad0cf0da60c Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Fri, 21 Apr 2017 20:48:46 +0200 Subject: [PATCH 011/627] Add AUTHORS.rst and SPONSORS.rst with links to the text files --- AUTHORS.rst | 3 +++ SPONSORS.rst | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 AUTHORS.rst create mode 100644 SPONSORS.rst diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 00000000..995327f4 --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,3 @@ +Check out our `AUTHORS`_. + +.. _AUTHORS: mycli/AUTHORS diff --git a/SPONSORS.rst b/SPONSORS.rst new file mode 100644 index 00000000..173555c3 --- /dev/null +++ b/SPONSORS.rst @@ -0,0 +1,3 @@ +Check out our `SPONSORS`_. + +.. _SPONSORS: mycli/SPONSORS From 393ee3185423a3c2b8c21acb6965c1b758629cc3 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 22 Apr 2017 12:16:20 -0500 Subject: [PATCH 012/627] Simplify author/sponsor file location code. --- mycli/main.py | 5 ++--- tests/test_main.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index b24adead..df3f789c 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -439,9 +439,8 @@ def run_cli(self): if self.smart_completion: self.refresh_completions() - project_root = os.path.join(os.path.dirname(PACKAGE_ROOT), 'mycli') - author_file = os.path.join(project_root, 'AUTHORS') - sponsor_file = os.path.join(project_root, 'SPONSORS') + author_file = os.path.join(PACKAGE_ROOT, 'AUTHORS') + sponsor_file = os.path.join(PACKAGE_ROOT, 'SPONSORS') key_binding_manager = mycli_bindings() diff --git a/tests/test_main.py b/tests/test_main.py index 56c0cb26..791fd5da 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -164,9 +164,8 @@ def test_confirm_destructive_query_notty(executor): assert confirm_destructive_query(sql) is None def test_thanks_picker_utf8(): - project_root = os.path.join(os.path.dirname(PACKAGE_ROOT), 'mycli') - author_file = os.path.join(project_root, 'AUTHORS') - sponsor_file = os.path.join(project_root, 'SPONSORS') + author_file = os.path.join(PACKAGE_ROOT, 'AUTHORS') + sponsor_file = os.path.join(PACKAGE_ROOT, 'SPONSORS') name = thanks_picker((author_file, sponsor_file)) assert isinstance(name, text_type) From d9490a7c7f080039eff0f8f53a0a59027846495d Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 22 Apr 2017 14:03:25 -0500 Subject: [PATCH 013/627] Move cryptography changelog item to appropriate section. --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index dfc4c727..c1643e0d 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ Internal Changes: * Rename tests/ to test/. (Thanks: [Dick Marinus]). * Move AUTHORS and SPONSORS to mycli directory. (Thanks: [Terje Røsten] []). +* Switch from pycryptodome to cryptography (Thanks: [Thomas Roten]). 1.10.0: ======= @@ -37,7 +38,6 @@ Internal Changes: * Test mycli using pexpect/python-behave (Thanks: [Dick Marinus]). * Run pep8 checks in travis (Thanks: [Irina Truong]). * Remove temporary hack for sqlparse (Thanks: [Dick Marinus]). -* Switch from pycryptodome to cryptography (Thanks: [Thomas Roten]). 1.9.0: ====== From 13df01d379a4469291d57155e2d8a714b2af9ad7 Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Tue, 25 Apr 2017 07:38:23 +0200 Subject: [PATCH 014/627] behave pager wrapper --- changelog.md | 1 + test/features/environment.py | 4 +++- test/features/steps/basic_commands.py | 2 ++ test/features/steps/crud_database.py | 8 ++++++-- test/features/steps/crud_table.py | 22 +++++++++++++++------- test/features/steps/iocommands.py | 2 +- test/features/steps/named_queries.py | 4 ++-- test/features/steps/specials.py | 6 +++++- test/features/steps/wrappers.py | 5 +++++ test/features/wrappager.py | 16 ++++++++++++++++ 10 files changed, 56 insertions(+), 14 deletions(-) create mode 100755 test/features/wrappager.py diff --git a/changelog.md b/changelog.md index b7cc8c3a..961eb54c 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ Internal Changes: * Rename tests/ to test/. (Thanks: [Dick Marinus]). * Move AUTHORS and SPONSORS to mycli directory. (Thanks: [Terje Røsten] []). +* Add pager wrapper for behave tests (Thanks: [Dick Marinus]). 1.10.0: ======= diff --git a/test/features/environment.py b/test/features/environment.py index e79e5740..376fce72 100644 --- a/test/features/environment.py +++ b/test/features/environment.py @@ -12,7 +12,6 @@ def before_all(context): """Set env parameters.""" os.environ['LINES'] = "100" os.environ['COLUMNS'] = "100" - os.environ['PAGER'] = 'cat' os.environ['EDITOR'] = 'ex' os.environ["COVERAGE_PROCESS_START"] = os.getcwd() + "/../.coveragerc" @@ -43,7 +42,10 @@ def before_all(context): 'dbname': db_name, 'dbname_tmp': db_name_full + '_tmp', 'vi': vi, + 'pager_boundary': '---boundary---', } + os.environ['PAGER'] = "{0} {1} {2}".format( + sys.executable, "test/features/wrappager.py", context.conf['pager_boundary']) context.cn = dbutils.create_db(context.conf['host'], context.conf['user'], context.conf['pass'], diff --git a/test/features/steps/basic_commands.py b/test/features/steps/basic_commands.py index 109472b8..845fea17 100644 --- a/test/features/steps/basic_commands.py +++ b/test/features/steps/basic_commands.py @@ -59,3 +59,5 @@ def step_send_help(context): """ context.cli.sendline('\\?') + wrappers.expect_exact( + context, context.conf['pager_boundary'] + '\r\n', timeout=5) diff --git a/test/features/steps/crud_database.py b/test/features/steps/crud_database.py index d7b8eeb2..a7616dad 100644 --- a/test/features/steps/crud_database.py +++ b/test/features/steps/crud_database.py @@ -73,20 +73,24 @@ def step_see_help(context): @then('we see database created') def step_see_db_created(context): """Wait to see create database output.""" - wrappers.expect_exact(context, 'Query OK, 1 row affected\r\n', timeout=2) + wrappers.expect_pager(context, 'Query OK, 1 row affected\r\n', timeout=2) @then('we see database dropped') def step_see_db_dropped(context): """Wait to see drop database output.""" - wrappers.expect_exact(context, 'Query OK, 0 rows affected\r\n', timeout=2) + wrappers.expect_pager(context, 'Query OK, 0 rows affected\r\n', timeout=2) @then('we see database connected') def step_see_db_connected(context): """Wait to see drop database output.""" + wrappers.expect_exact( + context, context.conf['pager_boundary'] + '\r\n', timeout=5) wrappers.expect_exact( context, 'You are now connected to database "', timeout=2) wrappers.expect_exact(context, '"', timeout=2) wrappers.expect_exact(context, ' as user "{0}"\r\n'.format( context.conf['user']), timeout=2) + wrappers.expect_exact( + context, context.conf['pager_boundary'] + '\r\n', timeout=5) diff --git a/test/features/steps/crud_table.py b/test/features/steps/crud_table.py index 34301c89..f6e9f044 100644 --- a/test/features/steps/crud_table.py +++ b/test/features/steps/crud_table.py @@ -9,6 +9,7 @@ import wrappers from behave import when, then +from textwrap import dedent @when('we create table') @@ -56,35 +57,42 @@ def step_drop_table(context): @then('we see table created') def step_see_table_created(context): """Wait to see create table output.""" - wrappers.expect_exact(context, 'Query OK, 0 rows affected\r\n', timeout=2) + wrappers.expect_pager(context, 'Query OK, 0 rows affected\r\n', timeout=2) @then('we see record inserted') def step_see_record_inserted(context): """Wait to see insert output.""" - wrappers.expect_exact(context, 'Query OK, 1 row affected\r\n', timeout=2) + wrappers.expect_pager(context, 'Query OK, 1 row affected\r\n', timeout=2) @then('we see record updated') def step_see_record_updated(context): """Wait to see update output.""" - wrappers.expect_exact(context, 'Query OK, 1 row affected\r\n', timeout=2) + wrappers.expect_pager(context, 'Query OK, 1 row affected\r\n', timeout=2) @then('we see data selected') def step_see_data_selected(context): """Wait to see select output.""" - wrappers.expect_exact( - context, '+-----+\r\n| x |\r\n+-----+\r\n| yyy |\r\n+-----+\r\n1 row in set\r\n', timeout=1) + wrappers.expect_pager( + context, dedent("""\ + +-----+\r + | x |\r + +-----+\r + | yyy |\r + +-----+\r + 1 row in set\r + """), timeout=1) @then('we see record deleted') def step_see_data_deleted(context): """Wait to see delete output.""" - wrappers.expect_exact(context, 'Query OK, 1 row affected\r\n', timeout=2) + wrappers.expect_pager(context, 'Query OK, 1 row affected\r\n', timeout=2) @then('we see table dropped') def step_see_table_dropped(context): """Wait to see drop output.""" - wrappers.expect_exact(context, 'Query OK, 0 rows affected\r\n', timeout=2) + wrappers.expect_pager(context, 'Query OK, 0 rows affected\r\n', timeout=2) diff --git a/test/features/steps/iocommands.py b/test/features/steps/iocommands.py index 73068fac..83cda6bc 100644 --- a/test/features/steps/iocommands.py +++ b/test/features/steps/iocommands.py @@ -23,7 +23,7 @@ def step_edit_type_sql(context): context.cli.sendline('i') context.cli.sendline('select * from abc') context.cli.sendline('.') - wrappers.expect_exact(context, ':', timeout=2) + wrappers.expect_exact(context, '\r\n:', timeout=2) @when('we exit the editor') diff --git a/test/features/steps/named_queries.py b/test/features/steps/named_queries.py index 60115c5c..40bf5bc7 100644 --- a/test/features/steps/named_queries.py +++ b/test/features/steps/named_queries.py @@ -32,7 +32,7 @@ def step_delete_named_query(context): @then('we see the named query saved') def step_see_named_query_saved(context): """Wait to see query saved.""" - wrappers.expect_exact(context, 'Saved.', timeout=1) + wrappers.expect_pager(context, 'Saved.\r\n', timeout=1) @then('we see the named query executed') @@ -45,4 +45,4 @@ def step_see_named_query_executed(context): @then('we see the named query deleted') def step_see_named_query_deleted(context): """Wait to see query deleted.""" - wrappers.expect_exact(context, 'foo: Deleted', timeout=1) + wrappers.expect_pager(context, 'foo: Deleted\r\n', timeout=1) diff --git a/test/features/steps/specials.py b/test/features/steps/specials.py index c0a3c0fe..f7715b9e 100644 --- a/test/features/steps/specials.py +++ b/test/features/steps/specials.py @@ -21,4 +21,8 @@ def step_refresh_completions(context): def step_see_refresh_started(context): """Wait to see refresh output.""" wrappers.expect_exact( - context, 'Auto-completion refresh started in the background', timeout=2) + context, context.conf['pager_boundary'] + '\r\n', timeout=5) + wrappers.expect_exact( + context, 'Auto-completion refresh started in the background.\r\n', timeout=2) + wrappers.expect_exact( + context, context.conf['pager_boundary'] + '\r\n', timeout=5) diff --git a/test/features/steps/wrappers.py b/test/features/steps/wrappers.py index aea74203..e8d9204a 100644 --- a/test/features/steps/wrappers.py +++ b/test/features/steps/wrappers.py @@ -14,3 +14,8 @@ def expect_exact(context, expected, timeout): raise Exception('Expected:\n---\n{0!r}\n---\n\nActual:\n---\n{1!r}\n---'.format( expected, actual)) + + +def expect_pager(context, expected, timeout): + expect_exact(context, "{0}\r\n{1}{0}\r\n".format( + context.conf['pager_boundary'], expected), timeout=timeout) diff --git a/test/features/wrappager.py b/test/features/wrappager.py new file mode 100755 index 00000000..51d49095 --- /dev/null +++ b/test/features/wrappager.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +import sys + + +def wrappager(boundary): + print(boundary) + while 1: + buf = sys.stdin.read(2048) + if not buf: + break + sys.stdout.write(buf) + print(boundary) + + +if __name__ == "__main__": + wrappager(sys.argv[1]) From 5074cccdbbb492d5416f6e7b540aae86f70332da Mon Sep 17 00:00:00 2001 From: Irina Truong Date: Wed, 26 Apr 2017 17:21:16 -0700 Subject: [PATCH 015/627] Fail on first error in travis script. --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 021be1c1..de590fc0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,12 +11,14 @@ install: - pip install git+https://github.com/hayd/pep8radius.git script: + - set -e - coverage run --source mycli -m py.test - cd test - behave - cd .. # check for pep8 errors, only looking at branch vs master. If there are errors, show diff and return an error code. - pep8radius master --docformatter --error-status || ( pep8radius master --docformatter --diff; false ) + - set +e after_success: - coverage combine From 724883308f60eb5ddba31d901bc9b437967a1f6e Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Fri, 28 Apr 2017 21:30:31 +0200 Subject: [PATCH 016/627] test using behave the source command --- changelog.md | 1 + test/features/basic_commands.feature | 6 ++++++ test/features/steps/basic_commands.py | 11 +++++++++++ 3 files changed, 18 insertions(+) diff --git a/changelog.md b/changelog.md index 961eb54c..1df05d69 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,7 @@ Internal Changes: * Rename tests/ to test/. (Thanks: [Dick Marinus]). * Move AUTHORS and SPONSORS to mycli directory. (Thanks: [Terje Røsten] []). * Add pager wrapper for behave tests (Thanks: [Dick Marinus]). +* Behave test source command (Thanks: [Dick Marinus]). 1.10.0: ======= diff --git a/test/features/basic_commands.feature b/test/features/basic_commands.feature index 227fe769..025b5850 100644 --- a/test/features/basic_commands.feature +++ b/test/features/basic_commands.feature @@ -12,6 +12,12 @@ Feature: run the cli, and we send "\?" command then we see help output + Scenario: run source command + When we run dbcli + and we wait for prompt + and we send source command + then we see help output + Scenario: run the cli and exit When we run dbcli and we wait for prompt diff --git a/test/features/steps/basic_commands.py b/test/features/steps/basic_commands.py index 845fea17..37f1e88a 100644 --- a/test/features/steps/basic_commands.py +++ b/test/features/steps/basic_commands.py @@ -8,6 +8,7 @@ from __future__ import unicode_literals import pexpect +import tempfile from behave import when import wrappers @@ -61,3 +62,13 @@ def step_send_help(context): context.cli.sendline('\\?') wrappers.expect_exact( context, context.conf['pager_boundary'] + '\r\n', timeout=5) + + +@when(u'we send source command') +def step_send_source_command(context): + with tempfile.NamedTemporaryFile() as f: + f.write(b'\?') + f.flush() + context.cli.sendline('\. {0}'.format(f.name)) + wrappers.expect_exact( + context, context.conf['pager_boundary'] + '\r\n', timeout=5) From b2b84bc8af9dd17baff9125ca67bddf6a50bd189 Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Mon, 1 May 2017 07:51:33 +0200 Subject: [PATCH 017/627] behave fix clean up In an earlier commit I've changed the current working directory and the removal of a temporary file didn't take that into account. --- changelog.md | 1 + test/features/steps/iocommands.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 1df05d69..8db8c1a1 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,7 @@ Internal Changes: * Move AUTHORS and SPONSORS to mycli directory. (Thanks: [Terje Røsten] []). * Add pager wrapper for behave tests (Thanks: [Dick Marinus]). * Behave test source command (Thanks: [Dick Marinus]). +* Behave fix clean up. (Thanks: [Dick Marinus]). 1.10.0: ======= diff --git a/test/features/steps/iocommands.py b/test/features/steps/iocommands.py index 83cda6bc..9a713a96 100644 --- a/test/features/steps/iocommands.py +++ b/test/features/steps/iocommands.py @@ -9,10 +9,12 @@ @when('we start external editor providing a file name') def step_edit_file(context): """Edit file with external editor.""" - context.editor_file_name = 'test_file_{0}.sql'.format(context.conf['vi']) + context.editor_file_name = '../test_file_{0}.sql'.format( + context.conf['vi']) if os.path.exists(context.editor_file_name): os.remove(context.editor_file_name) - context.cli.sendline('\e {0}'.format(context.editor_file_name)) + context.cli.sendline('\e {0}'.format( + os.path.basename(context.editor_file_name))) wrappers.expect_exact( context, 'Entering Ex mode. Type "visual" to go to Normal mode.', timeout=2) wrappers.expect_exact(context, '\r\n:', timeout=2) From e5b889ba39675de10da9e094ede4a552744c0c9d Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Mon, 1 May 2017 07:56:04 +0200 Subject: [PATCH 018/627] test using behave the tee command --- changelog.md | 1 + test/features/iocommands.feature | 11 +++++++++ test/features/steps/iocommands.py | 39 +++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/changelog.md b/changelog.md index 1df05d69..d031714f 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,7 @@ Internal Changes: * Move AUTHORS and SPONSORS to mycli directory. (Thanks: [Terje Røsten] []). * Add pager wrapper for behave tests (Thanks: [Dick Marinus]). * Behave test source command (Thanks: [Dick Marinus]). +* Test using behave the tee command (Thanks: [Dick Marinus]). 1.10.0: ======= diff --git a/test/features/iocommands.feature b/test/features/iocommands.feature index d043dc2e..4bcdf6e6 100644 --- a/test/features/iocommands.feature +++ b/test/features/iocommands.feature @@ -8,3 +8,14 @@ Feature: I/O commands and we exit the editor then we see dbcli prompt and we see the sql in prompt + + Scenario: tee output from query + When we run dbcli + and we wait for prompt + and we tee output + and we wait for prompt + and we query "select 123456" + and we wait for prompt + and we notee output + and we wait for prompt + then we see 123456 in tee output diff --git a/test/features/steps/iocommands.py b/test/features/steps/iocommands.py index 83cda6bc..c8293a10 100644 --- a/test/features/steps/iocommands.py +++ b/test/features/steps/iocommands.py @@ -4,6 +4,7 @@ import wrappers from behave import when, then +from textwrap import dedent @when('we start external editor providing a file name') @@ -41,3 +42,41 @@ def step_edit_done_sql(context): # Cleanup the edited file. if context.editor_file_name and os.path.exists(context.editor_file_name): os.remove(context.editor_file_name) + + +@when(u'we tee output') +def step_tee_ouptut(context): + context.tee_file_name = '../tee_file_{0}.sql'.format(context.conf['vi']) + if os.path.exists(context.tee_file_name): + os.remove(context.tee_file_name) + context.cli.sendline('tee {0}'.format( + os.path.basename(context.tee_file_name))) + wrappers.expect_pager(context, "\r\n", timeout=5) + + +@when(u'we query "select 123456"') +def step_query_select_123456(context): + context.cli.sendline('select 123456') + wrappers.expect_pager(context, dedent("""\ + +--------+\r + | 123456 |\r + +--------+\r + | 123456 |\r + +--------+\r + 1 row in set\r + """), timeout=5) + + +@when(u'we notee output') +def step_notee_output(context): + context.cli.sendline('notee') + wrappers.expect_pager(context, "\r\n", timeout=5) + + +@then(u'we see 123456 in tee output') +def step_see_123456_in_ouput(context): + with open(context.tee_file_name) as f: + assert '123456' in f.read() + if os.path.exists(context.tee_file_name): + os.remove(context.tee_file_name) + context.atprompt = True From 8c6e73842131658fd11ab9907cd59a31c7c8384d Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Thu, 27 Apr 2017 20:39:10 +0200 Subject: [PATCH 019/627] behave quit mycli nicely Before this patch mycli is killed by expect and it the coverage data cannot be written. --- changelog.md | 1 + test/features/environment.py | 20 ++++++++++++++++++-- test/features/steps/basic_commands.py | 4 +++- test/features/steps/crud_database.py | 5 ++++- test/features/steps/iocommands.py | 2 +- 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/changelog.md b/changelog.md index 18c33271..c47dd255 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ Internal Changes: * Behave test source command (Thanks: [Dick Marinus]). * Test using behave the tee command (Thanks: [Dick Marinus]). * Behave fix clean up. (Thanks: [Dick Marinus]). +* Behave quit mycli nicely (Thanks: [Dick Marinus]) 1.10.0: ======= diff --git a/test/features/environment.py b/test/features/environment.py index 376fce72..a0456b99 100644 --- a/test/features/environment.py +++ b/test/features/environment.py @@ -6,6 +6,7 @@ import sys import db_utils as dbutils import fixture_utils as fixutils +import pexpect def before_all(context): @@ -68,12 +69,27 @@ def after_all(context): # os.environ[k] = v +def before_step(context, _): + context.atprompt = False + + def after_scenario(context, _): """Cleans up after each test complete.""" if hasattr(context, 'cli') and not context.exit_sent: - # Terminate nicely. - context.cli.terminate() + # Quit nicely. + if not context.atprompt: + user = context.conf['user'] + host = context.conf['host'] + dbname = context.currentdb + context.cli.expect_exact( + 'mysql {0}@{1}:{2}> '.format( + user, host, dbname + ), + timeout=5 + ) + context.cli.sendcontrol('d') + context.cli.expect_exact(pexpect.EOF, timeout=5) # TODO: uncomment to debug a failure # def after_step(context, step): diff --git a/test/features/steps/basic_commands.py b/test/features/steps/basic_commands.py index 37f1e88a..df97ee0e 100644 --- a/test/features/steps/basic_commands.py +++ b/test/features/steps/basic_commands.py @@ -33,6 +33,7 @@ def step_run_cli(context): cmd = ' '.join(cmd_parts) context.cli = pexpect.spawnu(cmd, cwd='..') context.exit_sent = False + context.currentdb = context.conf['dbname'] @when('we wait for prompt') @@ -40,9 +41,10 @@ def step_wait_prompt(context): """Make sure prompt is displayed.""" user = context.conf['user'] host = context.conf['host'] - dbname = context.conf['dbname'] + dbname = context.currentdb wrappers.expect_exact(context, 'mysql {0}@{1}:{2}> '.format( user, host, dbname), timeout=5) + context.atprompt = True @when('we send "ctrl + d"') diff --git a/test/features/steps/crud_database.py b/test/features/steps/crud_database.py index a7616dad..af8580e1 100644 --- a/test/features/steps/crud_database.py +++ b/test/features/steps/crud_database.py @@ -39,12 +39,14 @@ def step_db_drop(context): def step_db_connect_test(context): """Send connect to database.""" db_name = context.conf['dbname'] + context.currentdb = db_name context.cli.sendline('use {0}'.format(db_name)) @when('we connect to dbserver') def step_db_connect_dbserver(context): """Send connect to database.""" + context.currentdb = 'mysql' context.cli.sendline('use mysql') @@ -59,9 +61,10 @@ def step_see_prompt(context): """Wait to see the prompt.""" user = context.conf['user'] host = context.conf['host'] - dbname = context.conf['dbname'] + dbname = context.currentdb wrappers.expect_exact(context, 'mysql {0}@{1}:{2}> '.format( user, host, dbname), timeout=5) + context.atprompt = True @then('we see help output') diff --git a/test/features/steps/iocommands.py b/test/features/steps/iocommands.py index 6641ce95..5de640ff 100644 --- a/test/features/steps/iocommands.py +++ b/test/features/steps/iocommands.py @@ -40,7 +40,7 @@ def step_edit_done_sql(context): for match in 'select * from abc'.split(' '): wrappers.expect_exact(context, match, timeout=1) # Cleanup the command line. - context.cli.sendcontrol('u') + context.cli.sendcontrol('c') # Cleanup the edited file. if context.editor_file_name and os.path.exists(context.editor_file_name): os.remove(context.editor_file_name) From 50bf54e04b00909f790457dd791d1c2b91a7d219 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Mon, 1 May 2017 16:43:59 -0500 Subject: [PATCH 020/627] Do not add time from multiple queries together. --- mycli/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index df3f789c..788e123e 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -535,8 +535,7 @@ def one_iteration(document=None): max_width) output.extend(formatted) - end = time() - total += end - start + total = time() - start mutating = mutating or is_mutating(status) except KeyboardInterrupt: # get last connection id From 82b65bbc08918037d4d4286eb3fc23927b55266b Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Mon, 1 May 2017 16:45:28 -0500 Subject: [PATCH 021/627] Add timing bugfix to changelog. --- changelog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/changelog.md b/changelog.md index 18c33271..11465d0c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,11 @@ TBD === +Bug Fixes: +---------- + +* Fixed incorrect timekeeping when running queries from a file. (Thanks: [Thomas Roten]). + Internal Changes: ----------------- From 88d470d2fda97dfae835ba7ac0ba99d997c3e907 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Mon, 1 May 2017 20:41:52 -0500 Subject: [PATCH 022/627] Add configurable reserved lines for completion menu. --- changelog.md | 5 +++++ mycli/main.py | 25 ++++++++++++++----------- mycli/myclirc | 3 +++ 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/changelog.md b/changelog.md index 18c33271..8422296d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,11 @@ TBD === +Features: +--------- + +* Add option to control how much space is reserved for the completion menu. (Thanks: [Thomas Roten]). + Internal Changes: ----------------- diff --git a/mycli/main.py b/mycli/main.py index df3f789c..043db60b 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -113,6 +113,7 @@ def __init__(self, sqlexecute=None, prompt=None, self.less_chatty = c['main'].as_bool('less_chatty') self.cli_style = c['colors'] self.wider_completion_menu = c['main'].as_bool('wider_completion_menu') + self.min_num_menu_lines = c['main'].as_int('min_num_menu_lines') c_dest_warning = c['main'].as_bool('destructive_warning') self.destructive_warning = c_dest_warning if warn is None else warn self.login_path_as_host = c['main'].as_bool('login_path_as_host') @@ -602,17 +603,19 @@ def one_iteration(document=None): get_toolbar_tokens = create_toolbar_tokens_func(self.completion_refresher.is_refreshing) - layout = create_prompt_layout(lexer=MyCliLexer, - multiline=True, - get_prompt_tokens=prompt_tokens, - get_continuation_tokens=get_continuation_tokens, - get_bottom_toolbar_tokens=get_toolbar_tokens, - display_completions_in_columns=self.wider_completion_menu, - extra_input_processors=[ - ConditionalProcessor( - processor=HighlightMatchingBracketProcessor(chars='[](){}'), - filter=HasFocus(DEFAULT_BUFFER) & ~IsDone()), - ]) + layout = create_prompt_layout( + lexer=MyCliLexer, + multiline=True, + get_prompt_tokens=prompt_tokens, + get_continuation_tokens=get_continuation_tokens, + get_bottom_toolbar_tokens=get_toolbar_tokens, + display_completions_in_columns=self.wider_completion_menu, + extra_input_processors=[ConditionalProcessor( + processor=HighlightMatchingBracketProcessor(chars='[](){}'), + filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() + )], + reserve_space_for_menu=self.min_num_menu_lines + ) with self._completer_lock: buf = CLIBuffer(always_multiline=self.multi_line, completer=self.completer, history=FileHistory(os.path.expanduser(os.environ.get('MYCLI_HISTFILE', '~/.mycli-history'))), diff --git a/mycli/myclirc b/mycli/myclirc index 01a11426..2f45778b 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -51,6 +51,9 @@ key_bindings = emacs # Enabling this option will show the suggestions in a wider menu. Thus more items are suggested. wider_completion_menu = False +# Number of lines to reserve for the completion menu. +min_num_menu_lines = 8 + # MySQL prompt # \t - Product type (Percona, MySQL, Mariadb) # \u - Username From b3a6839c69e2a37094ef894002e038ef0afc3df5 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Mon, 1 May 2017 21:09:15 -0500 Subject: [PATCH 023/627] Add current vi mode to toolbar. --- changelog.md | 5 +++++ mycli/clitoolbar.py | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 11465d0c..dab38caa 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,11 @@ TBD === +Features: +--------- + +* Display current vi mode in toolbar. (Thanks: [Thomas Roten]). + Bug Fixes: ---------- diff --git a/mycli/clitoolbar.py b/mycli/clitoolbar.py index b62d8edb..79f2a8a7 100644 --- a/mycli/clitoolbar.py +++ b/mycli/clitoolbar.py @@ -1,5 +1,6 @@ from pygments.token import Token from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode +from prompt_toolkit.key_binding.vi_state import InputMode def create_toolbar_tokens_func(get_is_refreshing): """ @@ -26,7 +27,10 @@ def get_toolbar_tokens(cli): ' (Semi-colon [;] will end the line)')) if cli.editing_mode == EditingMode.VI: - result.append((token.On, '[F4] Vi-mode')) + result.append(( + token.On, + '[F4] Vi-mode ({})'.format(_get_vi_mode(cli)) + )) else: result.append((token.On, '[F4] Emacs-mode')) @@ -35,3 +39,13 @@ def get_toolbar_tokens(cli): return result return get_toolbar_tokens + + +def _get_vi_mode(cli): + """Get the current vi mode for display.""" + return { + InputMode.INSERT: 'I', + InputMode.NAVIGATION: 'N', + InputMode.REPLACE: 'R', + InputMode.INSERT_MULTIPLE: 'M' + }[cli.vi_state.input_mode] From f75ea9bec59121346c20b298828071413dd5e54c Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Mon, 1 May 2017 22:22:17 -0500 Subject: [PATCH 024/627] Make reserved space automatically calculate. --- changelog.md | 2 +- mycli/main.py | 10 ++++++++-- mycli/myclirc | 3 --- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/changelog.md b/changelog.md index de0b1215..3819ce1a 100644 --- a/changelog.md +++ b/changelog.md @@ -4,7 +4,7 @@ TBD Features: --------- -* Add option to control how much space is reserved for the completion menu. (Thanks: [Thomas Roten]). +* Handle reserved space for completion menu better in small windows. (Thanks: [Thomas Roten]). Bug Fixes: ---------- diff --git a/mycli/main.py b/mycli/main.py index 5f36cc19..eff09340 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -113,7 +113,6 @@ def __init__(self, sqlexecute=None, prompt=None, self.less_chatty = c['main'].as_bool('less_chatty') self.cli_style = c['colors'] self.wider_completion_menu = c['main'].as_bool('wider_completion_menu') - self.min_num_menu_lines = c['main'].as_int('min_num_menu_lines') c_dest_warning = c['main'].as_bool('destructive_warning') self.destructive_warning = c_dest_warning if warn is None else warn self.login_path_as_host = c['main'].as_bool('login_path_as_host') @@ -613,7 +612,7 @@ def one_iteration(document=None): processor=HighlightMatchingBracketProcessor(chars='[](){}'), filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() )], - reserve_space_for_menu=self.min_num_menu_lines + reserve_space_for_menu=self.get_reserved_space() ) with self._completer_lock: buf = CLIBuffer(always_multiline=self.multi_line, completer=self.completer, @@ -750,6 +749,13 @@ def format_output(self, title, cur, headers, status, expanded=False, return output + def get_reserved_space(self): + """Get the number of lines to reserve for the completion menu.""" + reserved_space_ratio = .2 + max_reserved_space = 8 + _, height = click.get_terminal_size() + return min(int(height * reserved_space_ratio), max_reserved_space) + @click.command() @click.option('-h', '--host', envvar='MYSQL_HOST', help='Host address of the database.') diff --git a/mycli/myclirc b/mycli/myclirc index 2f45778b..01a11426 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -51,9 +51,6 @@ key_bindings = emacs # Enabling this option will show the suggestions in a wider menu. Thus more items are suggested. wider_completion_menu = False -# Number of lines to reserve for the completion menu. -min_num_menu_lines = 8 - # MySQL prompt # \t - Product type (Percona, MySQL, Mariadb) # \u - Username From 535bac41984342e39723ff32419aa3a0efa5375d Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Mon, 1 May 2017 23:03:34 -0500 Subject: [PATCH 025/627] Increase reserved space ratio. --- mycli/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index eff09340..5df93408 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -751,10 +751,10 @@ def format_output(self, title, cur, headers, status, expanded=False, def get_reserved_space(self): """Get the number of lines to reserve for the completion menu.""" - reserved_space_ratio = .2 + reserved_space_ratio = .45 max_reserved_space = 8 _, height = click.get_terminal_size() - return min(int(height * reserved_space_ratio), max_reserved_space) + return min(round(height * reserved_space_ratio), max_reserved_space) @click.command() From 7ec1c242d5b6a34c8c5e9a88a046b035de43cde2 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Mon, 1 May 2017 23:32:41 -0500 Subject: [PATCH 026/627] Add CLI Helpers dependency. --- mycli/main.py | 26 +++++++++++++------------- mycli/myclirc | 2 +- setup.py | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 5df93408..c3a4aef0 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -13,6 +13,7 @@ from random import choice from io import open +from cli_helpers.tabular_output import TabularOutputFormatter import click import sqlparse from prompt_toolkit import CommandLineInterface, Application, AbortAction @@ -37,7 +38,6 @@ from .config import (write_default_config, get_mylogin_cnf_path, open_mylogin_cnf, read_config_files, str_to_bool) from .key_bindings import mycli_bindings -from .output_formatter import output_formatter from .encodingutils import utf8tounicode from .lexer import MyCliLexer from .__init__ import __version__ @@ -107,7 +107,7 @@ def __init__(self, sqlexecute=None, prompt=None, self.multi_line = c['main'].as_bool('multi_line') self.key_bindings = c['main']['key_bindings'] special.set_timing_enabled(c['main'].as_bool('timing')) - self.formatter = output_formatter.OutputFormatter( + self.formatter = TabularOutputFormatter( format_name=c['main']['table_format']) self.syntax_style = c['main']['syntax_style'] self.less_chatty = c['main'].as_bool('less_chatty') @@ -149,7 +149,7 @@ def __init__(self, sqlexecute=None, prompt=None, self.smart_completion = c['main'].as_bool('smart_completion') self.completer = SQLCompleter( self.smart_completion, - supported_formats=self.formatter.supported_formats()) + supported_formats=self.formatter.supported_formats) self._completer_lock = threading.Lock() # Register custom special commands. @@ -185,13 +185,13 @@ def register_special_commands(self): def change_table_format(self, arg, **_): try: - self.formatter.set_format_name(arg) + self.formatter.format_name = arg yield (None, None, None, 'Changed table type to {}'.format(arg)) except ValueError: msg = 'Table type {} not yet implemented. Allowed types:'.format( arg) - for table_type in self.formatter.supported_formats(): + for table_type in self.formatter.supported_formats: msg += "\n\t{}".format(table_type) yield (None, None, None, msg) @@ -673,7 +673,7 @@ def refresh_completions(self, reset=False): self.completion_refresher.refresh( self.sqlexecute, self._on_completions_refreshed, {'smart_completion': self.smart_completion, - 'supported_formats': self.formatter.supported_formats()}) + 'supported_formats': self.formatter.supported_formats}) return [(None, None, None, 'Auto-completion refresh started in the background.')] @@ -726,7 +726,7 @@ def run_query(self, query, new_line=True): def format_output(self, title, cur, headers, status, expanded=False, max_width=None): - expanded = expanded or self.formatter.get_format_name() == 'expanded' + expanded = expanded or self.formatter.format_name == 'vertical' output = [] if title: # Only print the title if it's not None. @@ -735,12 +735,12 @@ def format_output(self, title, cur, headers, status, expanded=False, if cur: rows = list(cur) formatted = self.formatter.format_output( - rows, headers, format_name='expanded' if expanded else None) + rows, headers, format_name='vertical' if expanded else None) if (not expanded and max_width and rows and content_exceeds_width(rows[0], max_width) and headers): formatted = self.formatter.format_output( - rows, headers, format_name='expanded') + rows, headers, format_name='vertical') output.append(formatted) @@ -855,9 +855,9 @@ def cli(database, user, host, port, socket, password, dbname, if execute: try: if csv: - mycli.formatter.set_format_name('csv') + mycli.formatter.format_name = 'csv' elif not table: - mycli.formatter.set_format_name('tsv') + mycli.formatter.format_name = 'tsv' mycli.run_query(execute) exit(0) @@ -883,10 +883,10 @@ def cli(database, user, host, port, socket, password, dbname, new_line = True if csv: - mycli.formatter.set_format_name('csv') + mycli.formatter.format_name = 'csv' new_line = False elif not table: - mycli.formatter.set_format_name('tsv') + mycli.formatter.format_name = 'tsv' mycli.run_query(stdin_text, new_line=new_line) exit(0) diff --git a/mycli/myclirc b/mycli/myclirc index 01a11426..ab57a2ed 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -32,7 +32,7 @@ timing = True # Table format. Possible values: ascii, double, github, # psql, plain, simple, grid, fancy_grid, pipe, orgtbl, rst, mediawiki, html, -# latex, latex_booktabs, textile, moinmoin, jira, expanded, tsv, csv. +# latex, latex_booktabs, textile, moinmoin, jira, vertical, tsv, csv. # Recommended: ascii table_format = ascii diff --git a/setup.py b/setup.py index 27b761d9..5140b7e9 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ 'sqlparse>=0.2.2,<0.3.0', 'configobj >= 5.0.5', 'cryptography >= 1.0.0', - 'terminaltables >= 3.0.0', + 'cli_helpers >= 0.1.0', ] setup( From 2baed47e9397600c413af235163f3ec9318eadee Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Mon, 1 May 2017 23:33:02 -0500 Subject: [PATCH 027/627] Remove output formatter code and tests. --- mycli/output_formatter/__init__.py | 0 .../delimited_output_adapter.py | 28 - mycli/output_formatter/expanded.py | 34 - mycli/output_formatter/output_formatter.py | 95 -- mycli/output_formatter/preprocessors.py | 87 - mycli/output_formatter/tabulate_adapter.py | 22 - .../terminaltables_adapter.py | 25 - mycli/packages/tabulate.py | 1432 ----------------- test/test_expanded.py | 19 - test/test_output_formatter.py | 160 -- test/test_tabulate.py | 17 - 11 files changed, 1919 deletions(-) delete mode 100644 mycli/output_formatter/__init__.py delete mode 100644 mycli/output_formatter/delimited_output_adapter.py delete mode 100644 mycli/output_formatter/expanded.py delete mode 100644 mycli/output_formatter/output_formatter.py delete mode 100644 mycli/output_formatter/preprocessors.py delete mode 100644 mycli/output_formatter/tabulate_adapter.py delete mode 100644 mycli/output_formatter/terminaltables_adapter.py delete mode 100644 mycli/packages/tabulate.py delete mode 100644 test/test_expanded.py delete mode 100644 test/test_output_formatter.py delete mode 100644 test/test_tabulate.py diff --git a/mycli/output_formatter/__init__.py b/mycli/output_formatter/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mycli/output_formatter/delimited_output_adapter.py b/mycli/output_formatter/delimited_output_adapter.py deleted file mode 100644 index a01a2843..00000000 --- a/mycli/output_formatter/delimited_output_adapter.py +++ /dev/null @@ -1,28 +0,0 @@ -import contextlib -import csv -try: - from cStringIO import StringIO -except ImportError: - from io import StringIO - -from .preprocessors import override_missing_value, bytes_to_string - -supported_formats = ('csv', 'tsv') -preprocessors = (override_missing_value, bytes_to_string) - - -def adapter(data, headers, table_format='csv', **_): - """Wrap CSV formatting inside a standard function for OutputFormatter.""" - with contextlib.closing(StringIO()) as content: - if table_format == 'csv': - writer = csv.writer(content, delimiter=',') - elif table_format == 'tsv': - writer = csv.writer(content, delimiter='\t') - else: - raise ValueError('Invalid table_format specified.') - - writer.writerow(headers) - for row in data: - writer.writerow(row) - - return content.getvalue() diff --git a/mycli/output_formatter/expanded.py b/mycli/output_formatter/expanded.py deleted file mode 100644 index f77c1ee3..00000000 --- a/mycli/output_formatter/expanded.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Format data into a vertical, expanded table layout.""" - -from __future__ import unicode_literals - - -def get_separator(num): - """Get a row separator for row *num*.""" - return "{divider}[ {n}. row ]{divider}\n".format( - divider='*' * 27, n=num + 1) - - -def format_row(headers, row): - """Format a row.""" - formatted_row = [' | '.join(field) for field in zip(headers, row)] - return '\n'.join(formatted_row) - - -def expanded_table(rows, headers, **_): - """Format *rows* and *headers* as an expanded table. - - The values in *rows* and *headers* must be strings. - - """ - header_len = max([len(x) for x in headers]) - padded_headers = [x.ljust(header_len) for x in headers] - formatted_rows = [format_row(padded_headers, row) for row in rows] - - output = [] - for i, result in enumerate(formatted_rows): - output.append(get_separator(i)) - output.append(result) - output.append('\n') - - return ''.join(output) diff --git a/mycli/output_formatter/output_formatter.py b/mycli/output_formatter/output_formatter.py deleted file mode 100644 index 61e3c8d5..00000000 --- a/mycli/output_formatter/output_formatter.py +++ /dev/null @@ -1,95 +0,0 @@ -# -*- coding: utf-8 -*- -"""A generic output formatter interface.""" - -from __future__ import unicode_literals -from collections import namedtuple - -from .expanded import expanded_table -from .preprocessors import (override_missing_value, convert_to_string) - -from . import delimited_output_adapter -from . import tabulate_adapter -from . import terminaltables_adapter - -MISSING_VALUE = '' - -OutputFormatHandler = namedtuple( - 'OutputFormatHandler', - 'format_name preprocessors formatter formatter_args') - - -class OutputFormatter(object): - """A class with a standard interface for various formatting libraries.""" - - _output_formats = {} - - def __init__(self, format_name=None): - """Set the default *format_name*.""" - self._format_name = format_name - - def set_format_name(self, format_name): - """Set the OutputFormatter's default format.""" - if format_name in self.supported_formats(): - self._format_name = format_name - else: - raise ValueError('unrecognized format_name: {}'.format( - format_name)) - - def get_format_name(self): - """Get the OutputFormatter's default format.""" - return self._format_name - - def supported_formats(self): - """Return the supported output format names.""" - return tuple(self._output_formats.keys()) - - @classmethod - def register_new_formatter(cls, format_name, handler, preprocessors=(), - kwargs={}): - """Register a new formatter to format the output.""" - cls._output_formats[format_name] = OutputFormatHandler( - format_name, preprocessors, handler, kwargs) - - def format_output(self, data, headers, format_name=None, **kwargs): - """Format the headers and data using a specific formatter. - - *format_name* must be a formatter available in `supported_formats()`. - - All keyword arguments are passed to the specified formatter. - - """ - format_name = format_name or self._format_name - if format_name not in self.supported_formats(): - raise ValueError('unrecognized format: {}'.format(format_name)) - - (_, preprocessors, formatter, - fkwargs) = self._output_formats[format_name] - fkwargs.update(kwargs) - if preprocessors: - for f in preprocessors: - data, headers = f(data, headers, **fkwargs) - return formatter(data, headers, **fkwargs) - - -OutputFormatter.register_new_formatter('expanded', expanded_table, - (override_missing_value, - convert_to_string), - {'missing_value': MISSING_VALUE}) - -for delimiter_format in delimited_output_adapter.supported_formats: - OutputFormatter.register_new_formatter( - delimiter_format, delimited_output_adapter.adapter, - delimited_output_adapter.preprocessors, - {'table_format': delimiter_format, 'missing_value': MISSING_VALUE}) - -for tabulate_format in tabulate_adapter.supported_formats: - OutputFormatter.register_new_formatter( - tabulate_format, tabulate_adapter.adapter, - tabulate_adapter.preprocessors, - {'table_format': tabulate_format, 'missing_value': MISSING_VALUE}) - -for terminaltables_format in terminaltables_adapter.supported_formats: - OutputFormatter.register_new_formatter( - terminaltables_format, terminaltables_adapter.adapter, - terminaltables_adapter.preprocessors, - {'table_format': terminaltables_format, 'missing_value': MISSING_VALUE}) diff --git a/mycli/output_formatter/preprocessors.py b/mycli/output_formatter/preprocessors.py deleted file mode 100644 index 6f2e459c..00000000 --- a/mycli/output_formatter/preprocessors.py +++ /dev/null @@ -1,87 +0,0 @@ -from decimal import Decimal - -from mycli import encodingutils - - -def to_string(value): - """Convert *value* to a string.""" - if isinstance(value, encodingutils.binary_type): - return encodingutils.bytes_to_string(value) - else: - return encodingutils.text_type(value) - - -def convert_to_string(data, headers, **_): - """Convert all *data* and *headers* to strings.""" - return ([[to_string(v) for v in row] for row in data], - [to_string(h) for h in headers]) - - -def override_missing_value(data, headers, missing_value='', **_): - """Override missing values in the data with *missing_value*.""" - return ([[missing_value if v is None else v for v in row] for row in data], - headers) - - -def bytes_to_string(data, headers, **_): - """Convert all *data* and *headers* bytes to strings.""" - return ([[encodingutils.bytes_to_string(v) for v in row] for row in data], - [encodingutils.bytes_to_string(h) for h in headers]) - - -def intlen(value): - """Find (character) length. - - >>> intlen('11.1') - 2 - >>> intlen('11') - 2 - >>> intlen('1.1') - 1 - - """ - pos = value.find('.') - if pos < 0: - pos = len(value) - return pos - - -def align_decimals(data, headers, **_): - """Align decimals to decimal point.""" - pointpos = len(headers) * [0] - for row in data: - for i, v in enumerate(row): - if isinstance(v, Decimal): - v = encodingutils.text_type(v) - pointpos[i] = max(intlen(v), pointpos[i]) - results = [] - for row in data: - result = [] - for i, v in enumerate(row): - if isinstance(v, Decimal): - v = encodingutils.text_type(v) - result.append((pointpos[i] - intlen(v)) * " " + v) - else: - result.append(v) - results.append(result) - return results, headers - - -def quote_whitespaces(data, headers, quotestyle="'", **_): - """Quote leading/trailing whitespace.""" - quote = len(headers) * [False] - for row in data: - for i, v in enumerate(row): - v = encodingutils.text_type(v) - if v.startswith(' ') or v.endswith(' '): - quote[i] = True - - results = [] - for row in data: - result = [] - for i, v in enumerate(row): - quotation = quotestyle if quote[i] else '' - result.append('{quotestyle}{value}{quotestyle}'.format( - quotestyle=quotation, value=v)) - results.append(result) - return results, headers diff --git a/mycli/output_formatter/tabulate_adapter.py b/mycli/output_formatter/tabulate_adapter.py deleted file mode 100644 index b89dcc0b..00000000 --- a/mycli/output_formatter/tabulate_adapter.py +++ /dev/null @@ -1,22 +0,0 @@ -from mycli.packages import tabulate -from .preprocessors import bytes_to_string, align_decimals - -tabulate.PRESERVE_WHITESPACE = True - -supported_markup_formats = ('mediawiki', 'html', 'latex', 'latex_booktabs', - 'textile', 'moinmoin', 'jira') -supported_table_formats = ('plain', 'simple', 'grid', 'fancy_grid', 'pipe', - 'orgtbl', 'psql', 'rst') -supported_formats = supported_markup_formats + supported_table_formats - -preprocessors = (bytes_to_string, align_decimals) - - -def adapter(data, headers, table_format=None, missing_value='', **_): - """Wrap tabulate inside a standard function for OutputFormatter.""" - kwargs = {'tablefmt': table_format, 'missingval': missing_value, - 'disable_numparse': True} - if table_format in supported_markup_formats: - kwargs.update(numalign=None, stralign=None) - - return tabulate.tabulate(data, headers, **kwargs) diff --git a/mycli/output_formatter/terminaltables_adapter.py b/mycli/output_formatter/terminaltables_adapter.py deleted file mode 100644 index a8f50f98..00000000 --- a/mycli/output_formatter/terminaltables_adapter.py +++ /dev/null @@ -1,25 +0,0 @@ -import terminaltables - -from .preprocessors import (bytes_to_string, align_decimals, - override_missing_value) - -supported_formats = ('ascii', 'double', 'github') -preprocessors = (bytes_to_string, override_missing_value, align_decimals) - - -def adapter(data, headers, table_format=None, **_): - """Wrap terminaltables inside a standard function for OutputFormatter.""" - - table_format_handler = { - 'ascii': terminaltables.AsciiTable, - 'double': terminaltables.DoubleTable, - 'github': terminaltables.GithubFlavoredMarkdownTable, - } - - try: - table = table_format_handler[table_format] - except KeyError: - raise ValueError('unrecognized table format: {}'.format(table_format)) - - t = table([headers] + data) - return t.table diff --git a/mycli/packages/tabulate.py b/mycli/packages/tabulate.py deleted file mode 100644 index 1e67cea7..00000000 --- a/mycli/packages/tabulate.py +++ /dev/null @@ -1,1432 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Pretty-print tabular data.""" - -from __future__ import print_function -from __future__ import unicode_literals -from collections import namedtuple, Iterable -from platform import python_version_tuple -import re - - -if python_version_tuple()[0] < "3": - from itertools import izip_longest - from functools import partial - _none_type = type(None) - _bool_type = bool - _int_type = int - _long_type = long - _float_type = float - _text_type = unicode - _binary_type = str - - def _is_file(f): - return isinstance(f, file) - -else: - from itertools import zip_longest as izip_longest - from functools import reduce, partial - _none_type = type(None) - _bool_type = bool - _int_type = int - _long_type = int - _float_type = float - _text_type = str - _binary_type = bytes - basestring = str - - import io - - def _is_file(f): - return isinstance(f, io.IOBase) - -try: - import wcwidth # optional wide-character (CJK) support -except ImportError: - wcwidth = None - - -__all__ = ["tabulate", "tabulate_formats"] -__version__ = "0.8.0" - - -# minimum extra space in headers -MIN_PADDING = 2 - -PRESERVE_WHITESPACE = False - -_DEFAULT_FLOATFMT = "g" -_DEFAULT_MISSINGVAL = "" - - -# if True, enable wide-character (CJK) support -WIDE_CHARS_MODE = wcwidth is not None - - -Line = namedtuple("Line", ["begin", "hline", "sep", "end"]) - - -DataRow = namedtuple("DataRow", ["begin", "sep", "end"]) - - -# A table structure is suppposed to be: -# -# --- lineabove --------- -# headerrow -# --- linebelowheader --- -# datarow -# --- linebewteenrows --- -# ... (more datarows) ... -# --- linebewteenrows --- -# last datarow -# --- linebelow --------- -# -# TableFormat's line* elements can be -# -# - either None, if the element is not used, -# - or a Line tuple, -# - or a function: [col_widths], [col_alignments] -> string. -# -# TableFormat's *row elements can be -# -# - either None, if the element is not used, -# - or a DataRow tuple, -# - or a function: [cell_values], [col_widths], [col_alignments] -> string. -# -# padding (an integer) is the amount of white space around data values. -# -# with_header_hide: -# -# - either None, to display all table elements unconditionally, -# - or a list of elements not to be displayed if the table has column -# headers. -# -TableFormat = namedtuple("TableFormat", ["lineabove", "linebelowheader", - "linebetweenrows", "linebelow", - "headerrow", "datarow", - "padding", "with_header_hide"]) - - -def _pipe_segment_with_colons(align, colwidth): - """Return a segment of a horizontal line with optional colons which - indicate column's alignment (as in `pipe` output format).""" - w = colwidth - if align in ["right", "decimal"]: - return ('-' * (w - 1)) + ":" - elif align == "center": - return ":" + ('-' * (w - 2)) + ":" - elif align == "left": - return ":" + ('-' * (w - 1)) - else: - return '-' * w - - -def _pipe_line_with_colons(colwidths, colaligns): - """Return a horizontal line with optional colons to indicate column's - alignment (as in `pipe` output format).""" - segments = [_pipe_segment_with_colons(a, w) for a, w in - zip(colaligns, colwidths)] - return "|" + "|".join(segments) + "|" - - -def _mediawiki_row_with_attrs(separator, cell_values, colwidths, colaligns): - alignment = {"left": '', - "right": 'align="right"| ', - "center": 'align="center"| ', - "decimal": 'align="right"| '} - # hard-coded padding _around_ align attribute and value together - # rather than padding parameter which affects only the value - values_with_attrs = [' ' + alignment.get(a, '') + c + ' ' - for c, a in zip(cell_values, colaligns)] - colsep = separator*2 - return (separator + colsep.join(values_with_attrs)).rstrip() - - -def _textile_row_with_attrs(cell_values, colwidths, colaligns): - cell_values[0] += ' ' - alignment = {"left": "<.", "right": ">.", "center": "=.", "decimal": ">."} - values = (alignment.get(a, '') + v for a, v in zip(colaligns, cell_values)) - return '|' + '|'.join(values) + '|' - - -def _html_begin_table_without_header(colwidths_ignore, colaligns_ignore): - # this table header will be suppressed if there is a header row - return "\n".join(["", ""]) - - -def _html_row_with_attrs(celltag, cell_values, colwidths, colaligns): - alignment = {"left": '', - "right": ' style="text-align: right;"', - "center": ' style="text-align: center;"', - "decimal": ' style="text-align: right;"'} - values_with_attrs = ["<{0}{1}>{2}".format( - celltag, alignment.get(a, ''), c) for c, a in - zip(cell_values, colaligns)] - rowhtml = "" + "".join(values_with_attrs).rstrip() + "" - if celltag == "th": # it's a header row, create a new table header - rowhtml = "\n".join(["
", - "", - rowhtml, - "", - ""]) - return rowhtml - - -def _moin_row_with_attrs(celltag, cell_values, colwidths, colaligns, - header=''): - alignment = {"left": '', - "right": '', - "center": '', - "decimal": ''} - values_with_attrs = ["{0}{1} {2} ".format(celltag, - alignment.get(a, ''), - header + c + header) - for c, a in zip(cell_values, colaligns)] - return "".join(values_with_attrs) + "||" - - -def _latex_line_begin_tabular(colwidths, colaligns, booktabs=False): - alignment = {"left": "l", "right": "r", "center": "c", "decimal": "r"} - tabular_columns_fmt = "".join([alignment.get(a, "l") for a in colaligns]) - return "\n".join(["\\begin{tabular}{" + tabular_columns_fmt + "}", - "\\toprule" if booktabs else "\hline"]) - - -LATEX_ESCAPE_RULES = {r"&": r"\&", r"%": r"\%", r"$": r"\$", r"#": r"\#", - r"_": r"\_", r"^": r"\^{}", r"{": r"\{", r"}": r"\}", - r"~": r"\textasciitilde{}", "\\": r"\textbackslash{}", - r"<": r"\ensuremath{<}", r">": r"\ensuremath{>}"} - - -def _latex_row(cell_values, colwidths, colaligns, escrules=LATEX_ESCAPE_RULES): - def escape_char(c): - return escrules.get(c, c) - escaped_values = ["".join(map(escape_char, cell)) for cell in cell_values] - rowfmt = DataRow("", "&", "\\\\") - return _build_simple_row(escaped_values, rowfmt) - - -def _rst_escape_first_column(rows, headers): - def escape_empty(val): - if isinstance(val, (_text_type, _binary_type)) and val.strip() is "": - return ".." - else: - return val - new_headers = list(headers) - new_rows = [] - if headers: - new_headers[0] = escape_empty(headers[0]) - for row in rows: - new_row = list(row) - if new_row: - new_row[0] = escape_empty(row[0]) - new_rows.append(new_row) - return new_rows, new_headers - - -_table_formats = {"simple": - TableFormat( - lineabove=Line("", "-", " ", ""), - linebelowheader=Line("", "-", " ", ""), - linebetweenrows=None, - linebelow=Line("", "-", " ", ""), - headerrow=DataRow("", " ", ""), - datarow=DataRow("", " ", ""), - padding=0, - with_header_hide=["lineabove", "linebelow"]), - "plain": - TableFormat( - lineabove=None, linebelowheader=None, - linebetweenrows=None, linebelow=None, - headerrow=DataRow("", " ", ""), - datarow=DataRow("", " ", ""), - padding=0, with_header_hide=None), - "grid": - TableFormat( - lineabove=Line("+", "-", "+", "+"), - linebelowheader=Line("+", "=", "+", "+"), - linebetweenrows=Line("+", "-", "+", "+"), - linebelow=Line("+", "-", "+", "+"), - headerrow=DataRow("|", "|", "|"), - datarow=DataRow("|", "|", "|"), - padding=1, with_header_hide=None), - "fancy_grid": - TableFormat( - lineabove=Line("╒", "═", "╤", "╕"), - linebelowheader=Line("╞", "═", "╪", "╡"), - linebetweenrows=Line("├", "─", "┼", "┤"), - linebelow=Line("╘", "═", "╧", "╛"), - headerrow=DataRow("│", "│", "│"), - datarow=DataRow("│", "│", "│"), - padding=1, with_header_hide=None), - "pipe": - TableFormat( - lineabove=_pipe_line_with_colons, - linebelowheader=_pipe_line_with_colons, - linebetweenrows=None, - linebelow=None, - headerrow=DataRow("|", "|", "|"), - datarow=DataRow("|", "|", "|"), - padding=1, - with_header_hide=["lineabove"]), - "orgtbl": - TableFormat( - lineabove=None, - linebelowheader=Line("|", "-", "+", "|"), - linebetweenrows=None, - linebelow=None, - headerrow=DataRow("|", "|", "|"), - datarow=DataRow("|", "|", "|"), - padding=1, with_header_hide=None), - "jira": - TableFormat( - lineabove=None, - linebelowheader=None, - linebetweenrows=None, - linebelow=None, - headerrow=DataRow("||", "||", "||"), - datarow=DataRow("|", "|", "|"), - padding=1, with_header_hide=None), - "psql": - TableFormat( - lineabove=Line("+", "-", "+", "+"), - linebelowheader=Line("|", "-", "+", "|"), - linebetweenrows=None, - linebelow=Line("+", "-", "+", "+"), - headerrow=DataRow("|", "|", "|"), - datarow=DataRow("|", "|", "|"), - padding=1, with_header_hide=None), - "rst": - TableFormat( - lineabove=Line("", "=", " ", ""), - linebelowheader=Line("", "=", " ", ""), - linebetweenrows=None, - linebelow=Line("", "=", " ", ""), - headerrow=DataRow("", " ", ""), - datarow=DataRow("", " ", ""), - padding=0, with_header_hide=None), - "mediawiki": - TableFormat(lineabove=Line( - "{| class=\"wikitable\" style=\"text-align: left;\"", - "", "", "\n|+ \n|-"), - linebelowheader=Line("|-", "", "", ""), - linebetweenrows=Line("|-", "", "", ""), - linebelow=Line("|}", "", "", ""), - headerrow=partial(_mediawiki_row_with_attrs, "!"), - datarow=partial(_mediawiki_row_with_attrs, "|"), - padding=0, with_header_hide=None), - "moinmoin": - TableFormat( - lineabove=None, - linebelowheader=None, - linebetweenrows=None, - linebelow=None, - headerrow=partial(_moin_row_with_attrs, "||", - header="'''"), - datarow=partial(_moin_row_with_attrs, "||"), - padding=1, with_header_hide=None), - "html": - TableFormat( - lineabove=_html_begin_table_without_header, - linebelowheader="", - linebetweenrows=None, - linebelow=Line("\n
", "", "", ""), - headerrow=partial(_html_row_with_attrs, "th"), - datarow=partial(_html_row_with_attrs, "td"), - padding=0, with_header_hide=["lineabove"]), - "latex": - TableFormat( - lineabove=_latex_line_begin_tabular, - linebelowheader=Line("\\hline", "", "", ""), - linebetweenrows=None, - linebelow=Line("\\hline\n\\end{tabular}", "", "", ""), - headerrow=_latex_row, - datarow=_latex_row, - padding=1, with_header_hide=None), - "latex_raw": - TableFormat( - lineabove=_latex_line_begin_tabular, - linebelowheader=Line("\\hline", "", "", ""), - linebetweenrows=None, - linebelow=Line("\\hline\n\\end{tabular}", "", "", ""), - headerrow=partial(_latex_row, escrules={}), - datarow=partial(_latex_row, escrules={}), - padding=1, with_header_hide=None), - "latex_booktabs": - TableFormat( - lineabove=partial(_latex_line_begin_tabular, - booktabs=True), - linebelowheader=Line("\\midrule", "", "", ""), - linebetweenrows=None, - linebelow=Line("\\bottomrule\n\\end{tabular}", "", "", - ""), - headerrow=_latex_row, - datarow=_latex_row, - padding=1, with_header_hide=None), - "textile": - TableFormat( - lineabove=None, linebelowheader=None, - linebetweenrows=None, linebelow=None, - headerrow=DataRow("|_. ", "|_.", "|"), - datarow=_textile_row_with_attrs, - padding=1, with_header_hide=None)} - - -tabulate_formats = list(sorted(_table_formats.keys())) - - -# ANSI color codes -_invisible_codes = re.compile(r"\x1b\[\d+[;\d]*m|\x1b\[\d*\;\d*\;\d*m") -_invisible_codes_bytes = re.compile(b"\x1b\[\d+[;\d]*m|\x1b\[\d*\;\d*\;\d*m") - - -def _isconvertible(conv, string): - try: - n = conv(string) - return True - except (ValueError, TypeError): - return False - - -def _isnumber(string): - """ - >>> _isnumber("123.45") - True - >>> _isnumber("123") - True - >>> _isnumber("spam") - False - """ - return _isconvertible(float, string) - - -def _isint(string, inttype=int): - """ - >>> _isint("123") - True - >>> _isint("123.45") - False - """ - return type(string) is inttype or\ - (isinstance(string, _binary_type) or isinstance(string, _text_type))\ - and\ - _isconvertible(inttype, string) - - -def _isbool(string): - """ - >>> _isbool(True) - True - >>> _isbool("False") - True - >>> _isbool(1) - False - """ - return type(string) is _bool_type or\ - (isinstance(string, (_binary_type, _text_type)) and - string in ("True", "False")) - - -def _type(string, has_invisible=True, numparse=True): - """The least generic type (type(None), int, float, str, unicode). - - >>> _type(None) is type(None) - True - >>> _type("foo") is type("") - True - >>> _type("1") is type(1) - True - >>> _type('\x1b[31m42\x1b[0m') is type(42) - True - >>> _type('\x1b[31m42\x1b[0m') is type(42) - True - - """ - - if has_invisible and \ - (isinstance(string, _text_type) or isinstance(string, _binary_type)): - string = _strip_invisible(string) - - if string is None: - return _none_type - elif hasattr(string, "isoformat"): # datetime.datetime, date, and time - return _text_type - elif _isbool(string): - return _bool_type - elif _isint(string) and numparse: - return int - elif _isint(string, _long_type) and numparse: - return int - elif _isnumber(string) and numparse: - return float - elif isinstance(string, _binary_type): - return _binary_type - else: - return _text_type - - -def _afterpoint(string): - """Symbols after a decimal point, -1 if the string lacks the decimal point. - - >>> _afterpoint("123.45") - 2 - >>> _afterpoint("1001") - -1 - >>> _afterpoint("eggs") - -1 - >>> _afterpoint("123e45") - 2 - - """ - if _isnumber(string): - if _isint(string): - return -1 - else: - pos = string.rfind(".") - pos = string.lower().rfind("e") if pos < 0 else pos - if pos >= 0: - return len(string) - pos - 1 - else: - return -1 # no point - else: - return -1 # not a number - - -def _padleft(width, s): - """Flush right. - - >>> _padleft(6, '\u044f\u0439\u0446\u0430') == ' \u044f\u0439\u0446\u0430' - True - - """ - fmt = "{0:>%ds}" % width - return fmt.format(s) - - -def _padright(width, s): - """Flush left. - - >>> _padright(6, '\u044f\u0439\u0446\u0430') == '\u044f\u0439\u0446\u0430 ' - True - - """ - fmt = "{0:<%ds}" % width - return fmt.format(s) - - -def _padboth(width, s): - """Center string. - - >>> _padboth(6, '\u044f\u0439\u0446\u0430') == ' \u044f\u0439\u0446\u0430 ' - True - - """ - fmt = "{0:^%ds}" % width - return fmt.format(s) - - -def _strip_invisible(s): - "Remove invisible ANSI color codes." - if isinstance(s, _text_type): - return re.sub(_invisible_codes, "", s) - else: # a bytestring - return re.sub(_invisible_codes_bytes, "", s) - - -def _visible_width(s): - """Visible width of a printed string. ANSI color codes are removed. - - >>> _visible_width('\x1b[31mhello\x1b[0m'), _visible_width("world") - (5, 5) - - """ - # optional wide-character support - if wcwidth is not None and WIDE_CHARS_MODE: - len_fn = wcwidth.wcswidth - else: - len_fn = len - if isinstance(s, _text_type) or isinstance(s, _binary_type): - return len_fn(_strip_invisible(s)) - else: - return len_fn(_text_type(s)) - - -def _align_column(strings, alignment, minwidth=0, has_invisible=True): - """[string] -> [padded_string] - - >>> list(map(str,_align_column( - ... ["12.345", "-1234.5", "1.23", "1234.5", "1e+234", "1.0e234"], - ... "decimal"))) - [' 12.345 ', '-1234.5 ', ' 1.23 ', ' 1234.5 ', ' 1e+234 ', ' 1.0e234'] - - >>> list(map(str,_align_column(['123.4', '56.7890'], None))) - ['123.4', '56.7890'] - - """ - if alignment == "right": - if not PRESERVE_WHITESPACE: - strings = [s.strip() for s in strings] - padfn = _padleft - elif alignment == "center": - if not PRESERVE_WHITESPACE: - strings = [s.strip() for s in strings] - padfn = _padboth - elif alignment == "decimal": - if has_invisible: - decimals = [_afterpoint(_strip_invisible(s)) for s in strings] - else: - decimals = [_afterpoint(s) for s in strings] - maxdecimals = max(decimals) - strings = [s + (maxdecimals - decs) * " " - for s, decs in zip(strings, decimals)] - padfn = _padleft - elif not alignment: - return strings - else: - if not PRESERVE_WHITESPACE: - strings = [s.strip() for s in strings] - padfn = _padright - - enable_widechars = wcwidth is not None and WIDE_CHARS_MODE - if has_invisible: - width_fn = _visible_width - elif enable_widechars: # optional wide-character support if available - width_fn = wcwidth.wcswidth - else: - width_fn = len - - s_lens = list(map(len, strings)) - s_widths = list(map(width_fn, strings)) - maxwidth = max(max(s_widths), minwidth) - if not enable_widechars and not has_invisible: - padded_strings = [padfn(maxwidth, s) for s in strings] - else: - # enable wide-character width corrections - visible_widths = [maxwidth - (w - l) for w, l in zip(s_widths, s_lens)] - # wcswidth and _visible_width don't count invisible characters; - # padfn doesn't need to apply another correction - padded_strings = [padfn(w, s) for s, w in zip(strings, visible_widths)] - return padded_strings - - -def _more_generic(type1, type2): - types = {_none_type: 0, _bool_type: 1, int: 2, float: 3, _binary_type: 4, - _text_type: 5} - invtypes = {5: _text_type, 4: _binary_type, 3: float, 2: int, - 1: _bool_type, 0: _none_type} - moregeneric = max(types.get(type1, 5), types.get(type2, 5)) - return invtypes[moregeneric] - - -def _column_type(strings, has_invisible=True, numparse=True): - """The least generic type all column values are convertible to. - - >>> _column_type([True, False]) is _bool_type - True - >>> _column_type(["1", "2"]) is _int_type - True - >>> _column_type(["1", "2.3"]) is _float_type - True - >>> _column_type(["1", "2.3", "four"]) is _text_type - True - >>> _column_type(["four", '\u043f\u044f\u0442\u044c']) is _text_type - True - >>> _column_type([None, "brux"]) is _text_type - True - >>> _column_type([1, 2, None]) is _int_type - True - >>> import datetime as dt - >>> _column_type([dt.datetime(1991,2,19), dt.time(17,35)]) is _text_type - True - - """ - types = [_type(s, has_invisible, numparse) for s in strings] - return reduce(_more_generic, types, _bool_type) - - -def _format(val, valtype, floatfmt, missingval="", has_invisible=True): - """Format a value accoding to its type. - - Unicode is supported: - - >>> hrow = ['\u0431\u0443\u043a\u0432\u0430', - ... '\u0446\u0438\u0444\u0440\u0430'] - >>> tbl = [['\u0430\u0437', 2], ['\u0431\u0443\u043a\u0438', 4]] - >>> good_result = ('\\u0431\\u0443\\u043a\\u0432\\u0430 ' - ... '\\u0446\\u0438\\u0444\\u0440\\u0430\\n------- ' - ... '-------\\n\\u0430\\u0437 ' - ... '2\\n\\u0431\\u0443\\u043a\\u0438 4') - >>> tabulate(tbl, headers=hrow) == good_result - True - - """ - if val is None: - return missingval - - if valtype in [int, _text_type]: - return "{0}".format(val) - elif valtype is _binary_type: - try: - return _text_type(val, "ascii") - except TypeError: - return _text_type(val) - elif valtype is float: - is_a_colored_number = (has_invisible and - isinstance(val, (_text_type, _binary_type))) - if is_a_colored_number: - raw_val = _strip_invisible(val) - formatted_val = format(float(raw_val), floatfmt) - return val.replace(raw_val, formatted_val) - else: - return format(float(val), floatfmt) - else: - return "{0}".format(val) - - -def _align_header(header, alignment, width, visible_width): - """Pad string header to width chars given known visible_width of the - header.""" - width += len(header) - visible_width - if alignment == "left": - return _padright(width, header) - elif alignment == "center": - return _padboth(width, header) - elif not alignment: - return "{0}".format(header) - else: - return _padleft(width, header) - - -def _prepend_row_index(rows, index): - """Add a left-most index column.""" - if index is None or index is False: - return rows - if len(index) != len(rows): - print('index=', index) - print('rows=', rows) - raise ValueError('index must be as long as the number of data rows') - rows = [[v] + list(row) for v, row in zip(index, rows)] - return rows - - -def _bool(val): - """A wrapper around standard bool() which doesn't throw on NumPy - arrays.""" - try: - return bool(val) - except ValueError: # val is likely to be a numpy array with many elements - return False - - -def _normalize_tabular_data(tabular_data, headers, showindex="default"): - """Transform a supported data type to a list of lists, and a list of - headers. - - Supported tabular data types: - - * list-of-lists or another iterable of iterables - - * list of named tuples (usually used with headers="keys") - - * list of dicts (usually used with headers="keys") - - * list of OrderedDicts (usually used with headers="keys") - - * 2D NumPy arrays - - * NumPy record arrays (usually used with headers="keys") - - * dict of iterables (usually used with headers="keys") - - * pandas.DataFrame (usually used with headers="keys") - - The first row can be used as headers if headers="firstrow", - column indices can be used as headers if headers="keys". - - If showindex="default", show row indices of the pandas.DataFrame. - If showindex="always", show row indices for all types of data. - If showindex="never", don't show row indices for all types of data. - If showindex is an iterable, show its values as row indices. - - """ - - try: - bool(headers) - is_headers2bool_broken = False - except ValueError: # numpy.ndarray, pandas.core.index.Index, ... - is_headers2bool_broken = True - headers = list(headers) - - index = None - if hasattr(tabular_data, "keys") and hasattr(tabular_data, "values"): - # dict-like and pandas.DataFrame? - if hasattr(tabular_data.values, "__call__"): - # likely a conventional dict - keys = tabular_data.keys() - # columns have to be transposed - rows = list(izip_longest(*tabular_data.values())) - elif hasattr(tabular_data, "index"): - # values is a property, has .index => it's likely a - # pandas.DataFrame (pandas 0.11.0) - keys = list(tabular_data) - if tabular_data.index.name is not None: - if isinstance(tabular_data.index.name, list): - keys[:0] = tabular_data.index.name - else: - keys[:0] = [tabular_data.index.name] - # values matrix doesn't need to be transposed - vals = tabular_data.values - # for DataFrames add an index per default - index = list(tabular_data.index) - rows = [list(row) for row in vals] - else: - raise ValueError( - "tabular data doesn't appear to be a dict or a DataFrame") - - if headers == "keys": - headers = list(map(_text_type, keys)) # headers should be strings - - else: # it's a usual an iterable of iterables, or a NumPy array - rows = list(tabular_data) - - if (headers == "keys" and not rows): - # an empty table (issue #81) - headers = [] - elif (headers == "keys" and - hasattr(tabular_data, "dtype") and - getattr(tabular_data.dtype, "names")): - # numpy record array - headers = tabular_data.dtype.names - elif (headers == "keys" - and len(rows) > 0 - and isinstance(rows[0], tuple) - and hasattr(rows[0], "_fields")): - # namedtuple - headers = list(map(_text_type, rows[0]._fields)) - elif (len(rows) > 0 - and isinstance(rows[0], dict)): - # dict or OrderedDict - uniq_keys = set() # implements hashed lookup - keys = [] # storage for set - if headers == "firstrow": - firstdict = rows[0] if len(rows) > 0 else {} - keys.extend(firstdict.keys()) - uniq_keys.update(keys) - rows = rows[1:] - for row in rows: - for k in row.keys(): - # Save unique items in input order - if k not in uniq_keys: - keys.append(k) - uniq_keys.add(k) - if headers == 'keys': - headers = keys - elif isinstance(headers, dict): - # a dict of headers for a list of dicts - headers = [headers.get(k, k) for k in keys] - headers = list(map(_text_type, headers)) - elif headers == "firstrow": - if len(rows) > 0: - headers = [firstdict.get(k, k) for k in keys] - headers = list(map(_text_type, headers)) - else: - headers = [] - elif headers: - raise ValueError( - 'headers for a list of dicts is not a dict or a keyword') - rows = [[row.get(k) for k in keys] for row in rows] - - elif (headers == "keys" - and hasattr(tabular_data, "description") - and hasattr(tabular_data, "fetchone") - and hasattr(tabular_data, "rowcount")): - # Python Database API cursor object (PEP 0249) - # print tabulate(cursor, headers='keys') - headers = [column[0] for column in tabular_data.description] - - elif headers == "keys" and len(rows) > 0: - # keys are column indices - headers = list(map(_text_type, range(len(rows[0])))) - - # take headers from the first row if necessary - if headers == "firstrow" and len(rows) > 0: - if index is not None: - headers = [index[0]] + list(rows[0]) - index = index[1:] - else: - headers = rows[0] - headers = list(map(_text_type, headers)) # headers should be strings - rows = rows[1:] - - headers = list(map(_text_type, headers)) - rows = list(map(list, rows)) - - # add or remove an index column - showindex_is_a_str = type(showindex) in [_text_type, _binary_type] - if showindex == "default" and index is not None: - rows = _prepend_row_index(rows, index) - elif isinstance(showindex, Iterable) and not showindex_is_a_str: - rows = _prepend_row_index(rows, list(showindex)) - elif (showindex == "always" or - (_bool(showindex) and not showindex_is_a_str)): - if index is None: - index = list(range(len(rows))) - rows = _prepend_row_index(rows, index) - elif (showindex == "never" or - (not _bool(showindex) and not showindex_is_a_str)): - pass - - # pad with empty headers for initial columns if necessary - if headers and len(rows) > 0: - nhs = len(headers) - ncols = len(rows[0]) - if nhs < ncols: - headers = [""] * (ncols - nhs) + headers - - return rows, headers - - -def tabulate(tabular_data, headers=(), tablefmt="simple", - floatfmt=_DEFAULT_FLOATFMT, numalign="decimal", stralign="left", - missingval=_DEFAULT_MISSINGVAL, showindex="default", - disable_numparse=False): - """Format a fixed width table for pretty printing. - - >>> print(tabulate([[1, 2.34], [-56, "8.999"], ["2", "10001"]])) - --- --------- - 1 2.34 - -56 8.999 - 2 10001 - --- --------- - - The first required argument (`tabular_data`) can be a - list-of-lists (or another iterable of iterables), a list of named - tuples, a dictionary of iterables, an iterable of dictionaries, - a two-dimensional NumPy array, NumPy record array, or a Pandas' - dataframe. - - - Table headers - ------------- - - To print nice column headers, supply the second argument (`headers`): - - - `headers` can be an explicit list of column headers - - if `headers="firstrow"`, then the first row of data is used - - if `headers="keys"`, then dictionary keys or column indices are used - - Otherwise a headerless table is produced. - - If the number of headers is less than the number of columns, they - are supposed to be names of the last columns. This is consistent - with the plain-text format of R and Pandas' dataframes. - - >>> print(tabulate([["sex","age"],["Alice","F",24],["Bob","M",19]], - ... headers="firstrow")) - sex age - ----- ----- ----- - Alice F 24 - Bob M 19 - - By default, pandas.DataFrame data have an additional column called - row index. To add a similar column to all other types of data, - use `showindex="always"` or `showindex=True`. To suppress row indices - for all types of data, pass `showindex="never" or `showindex=False`. - To add a custom row index column, pass `showindex=some_iterable`. - - >>> print(tabulate([["F",24],["M",19]], showindex="always")) - - - -- - 0 F 24 - 1 M 19 - - - -- - - - Column alignment - ---------------- - - `tabulate` tries to detect column types automatically, and aligns - the values properly. By default it aligns decimal points of the - numbers (or flushes integer numbers to the right), and flushes - everything else to the left. Possible column alignments - (`numalign`, `stralign`) are: "right", "center", "left", "decimal" - (only for `numalign`), and None (to disable alignment). - - - Table formats - ------------- - - `floatfmt` is a format specification used for columns which - contain numeric data with a decimal point. This can also be - a list or tuple of format strings, one per column. - - `None` values are replaced with a `missingval` string (like - `floatfmt`, this can also be a list of values for different - columns): - - >>> print(tabulate([["spam", 1, None], - ... ["eggs", 42, 3.14], - ... ["other", None, 2.7]], missingval="?")) - ----- -- ---- - spam 1 ? - eggs 42 3.14 - other ? 2.7 - ----- -- ---- - - Various plain-text table formats (`tablefmt`) are supported: - 'plain', 'simple', 'grid', 'pipe', 'orgtbl', 'rst', 'mediawiki', - 'latex', 'latex_raw' and 'latex_booktabs'. Variable `tabulate_formats` - contains the list of currently supported formats. - - "plain" format doesn't use any pseudographics to draw tables, - it separates columns with a double space: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... ["strings", "numbers"], "plain")) - strings numbers - spam 41.9999 - eggs 451 - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... tablefmt="plain")) - spam 41.9999 - eggs 451 - - "simple" format is like Pandoc simple_tables: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... ["strings", "numbers"], "simple")) - strings numbers - --------- --------- - spam 41.9999 - eggs 451 - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... tablefmt="simple")) - ---- -------- - spam 41.9999 - eggs 451 - ---- -------- - - "grid" is similar to tables produced by Emacs table.el package or - Pandoc grid_tables: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... ["strings", "numbers"], "grid")) - +-----------+-----------+ - | strings | numbers | - +===========+===========+ - | spam | 41.9999 | - +-----------+-----------+ - | eggs | 451 | - +-----------+-----------+ - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... tablefmt="grid")) - +------+----------+ - | spam | 41.9999 | - +------+----------+ - | eggs | 451 | - +------+----------+ - - "fancy_grid" draws a grid using box-drawing characters: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... ["strings", "numbers"], "fancy_grid")) - ╒═══════════╤═══════════╕ - │ strings │ numbers │ - ╞═══════════╪═══════════╡ - │ spam │ 41.9999 │ - ├───────────┼───────────┤ - │ eggs │ 451 │ - ╘═══════════╧═══════════╛ - - "pipe" is like tables in PHP Markdown Extra extension or Pandoc - pipe_tables: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... ["strings", "numbers"], "pipe")) - | strings | numbers | - |:----------|----------:| - | spam | 41.9999 | - | eggs | 451 | - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... tablefmt="pipe")) - |:-----|---------:| - | spam | 41.9999 | - | eggs | 451 | - - "orgtbl" is like tables in Emacs org-mode and orgtbl-mode. They - are slightly different from "pipe" format by not using colons to - define column alignment, and using a "+" sign to indicate line - intersections: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... ["strings", "numbers"], "orgtbl")) - | strings | numbers | - |-----------+-----------| - | spam | 41.9999 | - | eggs | 451 | - - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... tablefmt="orgtbl")) - | spam | 41.9999 | - | eggs | 451 | - - "rst" is like a simple table format from reStructuredText; please - note that reStructuredText accepts also "grid" tables: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... ["strings", "numbers"], "rst")) - ========= ========= - strings numbers - ========= ========= - spam 41.9999 - eggs 451 - ========= ========= - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="rst")) - ==== ======== - spam 41.9999 - eggs 451 - ==== ======== - - "mediawiki" produces a table markup used in Wikipedia and on other - MediaWiki-based sites: - - >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], - ... ["eggs", "451.0"]], headers="firstrow", - ... tablefmt="mediawiki")) - {| class="wikitable" style="text-align: left;" - |+ - |- - ! strings !! align="right"| numbers - |- - | spam || align="right"| 41.9999 - |- - | eggs || align="right"| 451 - |} - - "html" produces HTML markup: - - >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], - ... ["eggs", "451.0"]], headers="firstrow", - ... tablefmt="html")) - - - - - - - - -
strings numbers
spam 41.9999
eggs 451
- - "latex" produces a tabular environment of LaTeX document markup: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... tablefmt="latex")) - \\begin{tabular}{lr} - \\hline - spam & 41.9999 \\\\ - eggs & 451 \\\\ - \\hline - \\end{tabular} - - "latex_raw" is similar to "latex", but doesn't escape special characters, - such as backslash and underscore, so LaTeX commands may embedded into - cells' values: - - >>> print(tabulate([["spam$_9$", 41.9999], ["\\\\emph{eggs}", "451.0"]], - ... tablefmt="latex_raw")) - \\begin{tabular}{lr} - \\hline - spam$_9$ & 41.9999 \\\\ - \\emph{eggs} & 451 \\\\ - \\hline - \\end{tabular} - - "latex_booktabs" produces a tabular environment of LaTeX document markup - using the booktabs.sty package: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... tablefmt="latex_booktabs")) - \\begin{tabular}{lr} - \\toprule - spam & 41.9999 \\\\ - eggs & 451 \\\\ - \\bottomrule - \end{tabular} - - Number parsing - -------------- - By default, anything which can be parsed as a number is a number. - This ensures numbers represented as strings are aligned properly. - This can lead to weird results for particular strings such as - specific git SHAs e.g. "42992e1" will be parsed into the number - 429920 and aligned as such. - - To completely disable number parsing (and alignment), use - `disable_numparse=True`. For more fine grained control, a list column - indices is used to disable number parsing only on those columns - e.g. `disable_numparse=[0, 2]` would disable number parsing only on the - first and third columns. - - """ - if tabular_data is None: - tabular_data = [] - list_of_lists, headers = _normalize_tabular_data( - tabular_data, headers, showindex=showindex) - - # empty values in the first column of RST tables should be escaped - # (issue #82). "" should be escaped as "\\ " or ".." - if tablefmt == 'rst': - list_of_lists, headers = _rst_escape_first_column(list_of_lists, - headers) - - # optimization: look for ANSI control codes once, - # enable smart width functions only if a control code is found - plain_text = '\n'.join(['\t'.join(map(_text_type, headers))] + - ['\t'.join(map(_text_type, row)) - for row in list_of_lists]) - - has_invisible = re.search(_invisible_codes, plain_text) - enable_widechars = wcwidth is not None and WIDE_CHARS_MODE - if has_invisible: - width_fn = _visible_width - elif enable_widechars: # optional wide-character support if available - width_fn = wcwidth.wcswidth - else: - width_fn = len - - # format rows and columns, convert numeric values to strings - cols = list(izip_longest(*list_of_lists)) - numparses = _expand_numparse(disable_numparse, len(cols)) - coltypes = [_column_type(col, numparse=np) for col, np in - zip(cols, numparses)] - if isinstance(floatfmt, basestring): # old version - # just duplicate the string to use in each column - float_formats = len(cols) * [floatfmt] - else: # if floatfmt is list, tuple etc we have one per column - float_formats = list(floatfmt) - if len(float_formats) < len(cols): - float_formats.extend((len(cols) - len(float_formats)) * - [_DEFAULT_FLOATFMT]) - if isinstance(missingval, basestring): - missing_vals = len(cols) * [missingval] - else: - missing_vals = list(missingval) - if len(missing_vals) < len(cols): - missing_vals.extend((len(cols) - len(missing_vals)) * - [_DEFAULT_MISSINGVAL]) - cols = [[_format(v, ct, fl_fmt, miss_v, has_invisible) for v in c] - for c, ct, fl_fmt, miss_v in zip(cols, coltypes, float_formats, - missing_vals)] - - # align columns - aligns = [numalign if ct in [int, float] else stralign for ct in coltypes] - minwidths = [width_fn(h) + MIN_PADDING - for h in headers] if headers else [0] * len(cols) - cols = [_align_column(c, a, minw, has_invisible) - for c, a, minw in zip(cols, aligns, minwidths)] - - if headers: - # align headers and add headers - t_cols = cols or [['']] * len(headers) - t_aligns = aligns or [stralign] * len(headers) - minwidths = [max(minw, width_fn(c[0])) - for minw, c in zip(minwidths, t_cols)] - headers = [_align_header(h, a, minw, width_fn(h)) - for h, a, minw in zip(headers, t_aligns, minwidths)] - rows = list(zip(*cols)) - else: - minwidths = [width_fn(c[0]) for c in cols] - rows = list(zip(*cols)) - - if not isinstance(tablefmt, TableFormat): - tablefmt = _table_formats.get(tablefmt, _table_formats["simple"]) - - return _format_table(tablefmt, headers, rows, minwidths, aligns) - - -def _expand_numparse(disable_numparse, column_count): - """Return a list of bools of length `column_count` which indicates whether - number parsing should be used on each column. - - If `disable_numparse` is a list of indices, each of those indices - are False, and everything else is True. If `disable_numparse` is a - bool, then the returned list is all the same. - - """ - if isinstance(disable_numparse, Iterable): - numparses = [True] * column_count - for index in disable_numparse: - numparses[index] = False - return numparses - else: - return [not disable_numparse] * column_count - - -def _build_simple_row(padded_cells, rowfmt): - "Format row according to DataRow format without padding." - begin, sep, end = rowfmt - return (begin + sep.join(padded_cells) + end).rstrip() - - -def _build_row(padded_cells, colwidths, colaligns, rowfmt): - "Return a string which represents a row of data cells." - if not rowfmt: - return None - if hasattr(rowfmt, "__call__"): - return rowfmt(padded_cells, colwidths, colaligns) - else: - return _build_simple_row(padded_cells, rowfmt) - - -def _build_line(colwidths, colaligns, linefmt): - "Return a string which represents a horizontal line." - if not linefmt: - return None - if hasattr(linefmt, "__call__"): - return linefmt(colwidths, colaligns) - else: - begin, fill, sep, end = linefmt - cells = [fill*w for w in colwidths] - return _build_simple_row(cells, (begin, sep, end)) - - -def _pad_row(cells, padding): - if cells: - pad = " "*padding - padded_cells = [pad + cell + pad for cell in cells] - return padded_cells - else: - return cells - - -def _format_table(fmt, headers, rows, colwidths, colaligns): - """Produce a plain-text representation of the table.""" - lines = [] - hidden = fmt.with_header_hide if (headers and fmt.with_header_hide) else [] - pad = fmt.padding - headerrow = fmt.headerrow - - padded_widths = [(w + 2*pad) for w in colwidths] - padded_headers = _pad_row(headers, pad) - padded_rows = [_pad_row(row, pad) for row in rows] - - if fmt.lineabove and "lineabove" not in hidden: - lines.append(_build_line(padded_widths, colaligns, fmt.lineabove)) - - if padded_headers: - lines.append(_build_row(padded_headers, padded_widths, colaligns, - headerrow)) - if fmt.linebelowheader and "linebelowheader" not in hidden: - lines.append(_build_line(padded_widths, colaligns, - fmt.linebelowheader)) - - if padded_rows and fmt.linebetweenrows and "linebetweenrows" not in hidden: - # initial rows with a line below - for row in padded_rows[:-1]: - lines.append(_build_row(row, padded_widths, colaligns, - fmt.datarow)) - lines.append(_build_line(padded_widths, colaligns, - fmt.linebetweenrows)) - # the last row without a line below - lines.append(_build_row(padded_rows[-1], padded_widths, colaligns, - fmt.datarow)) - else: - for row in padded_rows: - lines.append(_build_row(row, padded_widths, colaligns, - fmt.datarow)) - - if fmt.linebelow and "linebelow" not in hidden: - lines.append(_build_line(padded_widths, colaligns, fmt.linebelow)) - - if headers or rows: - return "\n".join(lines) - else: # a completely empty table - return "" - - -def _main(): - """\ Usage: tabulate [options] [FILE ...] - - Pretty-print tabular data. - See also https://bitbucket.org/astanin/python-tabulate - - FILE a filename of the file with tabular data; - if "-" or missing, read data from stdin. - - Options: - - -h, --help show this message - -1, --header use the first row of data as a table header - -o FILE, --output FILE print table to FILE (default: stdout) - -s REGEXP, --sep REGEXP use a custom column separator (default: whitespace) - -F FPFMT, --float FPFMT floating point number format (default: g) - -f FMT, --format FMT set output table format; supported formats: - plain, simple, grid, fancy_grid, pipe, orgtbl, - rst, mediawiki, html, latex, latex_raw, - latex_booktabs, tsv - (default: simple) - - """ - import getopt - import sys - import textwrap - usage = textwrap.dedent(_main.__doc__) - try: - opts, args = getopt.getopt( - sys.argv[1:], "h1o:s:F:f:", - ["help", "header", "output", "sep=", "float=", "format="]) - except getopt.GetoptError as e: - print(e) - print(usage) - sys.exit(2) - headers = [] - floatfmt = _DEFAULT_FLOATFMT - tablefmt = "simple" - sep = r"\s+" - outfile = "-" - for opt, value in opts: - if opt in ["-1", "--header"]: - headers = "firstrow" - elif opt in ["-o", "--output"]: - outfile = value - elif opt in ["-F", "--float"]: - floatfmt = value - elif opt in ["-f", "--format"]: - if value not in tabulate_formats: - print("%s is not a supported table format" % value) - print(usage) - sys.exit(3) - tablefmt = value - elif opt in ["-s", "--sep"]: - sep = value - elif opt in ["-h", "--help"]: - print(usage) - sys.exit(0) - files = [sys.stdin] if not args else args - with (sys.stdout if outfile == "-" else open(outfile, "w")) as out: - for f in files: - if f == "-": - f = sys.stdin - if _is_file(f): - _pprint_file(f, headers=headers, tablefmt=tablefmt, - sep=sep, floatfmt=floatfmt, file=out) - else: - with open(f) as fobj: - _pprint_file(fobj, headers=headers, tablefmt=tablefmt, - sep=sep, floatfmt=floatfmt, file=out) - - -def _pprint_file(fobject, headers, tablefmt, sep, floatfmt, file): - rows = fobject.readlines() - table = [re.split(sep, r.rstrip()) for r in rows if r.strip()] - print(tabulate(table, headers, tablefmt, floatfmt=floatfmt), file=file) - - -if __name__ == "__main__": - _main() diff --git a/test/test_expanded.py b/test/test_expanded.py deleted file mode 100644 index 7233e91c..00000000 --- a/test/test_expanded.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Test the vertical, expanded table formatter.""" -from textwrap import dedent - -from mycli.output_formatter.expanded import expanded_table -from mycli.encodingutils import text_type - - -def test_expanded_table_renders(): - results = [('hello', text_type(123)), ('world', text_type(456))] - - expected = dedent("""\ - ***************************[ 1. row ]*************************** - name | hello - age | 123 - ***************************[ 2. row ]*************************** - name | world - age | 456 - """) - assert expected == expanded_table(results, ('name', 'age')) diff --git a/test/test_output_formatter.py b/test/test_output_formatter.py deleted file mode 100644 index 9844c191..00000000 --- a/test/test_output_formatter.py +++ /dev/null @@ -1,160 +0,0 @@ -# -*- coding: utf-8 -*- -"""Test the generic output formatter interface.""" - -from __future__ import unicode_literals -from decimal import Decimal -from textwrap import dedent - -from mycli.output_formatter.preprocessors import (align_decimals, - bytes_to_string, - convert_to_string, - quote_whitespaces, - override_missing_value, - to_string) -from mycli.output_formatter.output_formatter import OutputFormatter -from mycli.output_formatter import delimited_output_adapter -from mycli.output_formatter import tabulate_adapter -from mycli.output_formatter import terminaltables_adapter - - -def test_to_string(): - """Test the *output_formatter.to_string()* function.""" - assert 'a' == to_string('a') - assert 'a' == to_string(b'a') - assert '1' == to_string(1) - assert '1.23' == to_string(1.23) - - -def test_convert_to_string(): - """Test the *output_formatter.convert_to_string()* function.""" - data = [[1, 'John'], [2, 'Jill']] - headers = [0, 'name'] - expected = ([['1', 'John'], ['2', 'Jill']], ['0', 'name']) - - assert expected == convert_to_string(data, headers) - - -def test_override_missing_values(): - """Test the *output_formatter.override_missing_values()* function.""" - data = [[1, None], [2, 'Jill']] - headers = [0, 'name'] - expected = ([[1, ''], [2, 'Jill']], [0, 'name']) - - assert expected == override_missing_value(data, headers, - missing_value='') - - -def test_bytes_to_string(): - """Test the *output_formatter.bytes_to_string()* function.""" - data = [[1, 'John'], [2, b'Jill']] - headers = [0, 'name'] - expected = ([[1, 'John'], [2, 'Jill']], [0, 'name']) - - assert expected == bytes_to_string(data, headers) - - -def test_align_decimals(): - """Test the *align_decimals()* function.""" - data = [[Decimal('200'), Decimal('1')], [ - Decimal('1.00002'), Decimal('1.0')]] - headers = ['num1', 'num2'] - expected = ([['200', '1'], [' 1.00002', '1.0']], ['num1', 'num2']) - - assert expected == align_decimals(data, headers) - - -def test_align_decimals_empty_result(): - """Test *align_decimals()* with no results.""" - data = [] - headers = ['num1', 'num2'] - expected = ([], ['num1', 'num2']) - - assert expected == align_decimals(data, headers) - - -def test_quote_whitespaces(): - """Test the *quote_whitespaces()* function.""" - data = [[" before", "after "], [" both ", "none"]] - headers = ['h1', 'h2'] - expected = ([["' before'", "'after '"], ["' both '", "'none'"]], - ['h1', 'h2']) - - assert expected == quote_whitespaces(data, headers) - - -def test_quote_whitespaces_empty_result(): - """Test the *quote_whitespaces()* function with no results.""" - data = [] - headers = ['h1', 'h2'] - expected = ([], ['h1', 'h2']) - - assert expected == quote_whitespaces(data, headers) - - -def test_tabulate_wrapper(): - """Test the *output_formatter.tabulate_wrapper()* function.""" - data = [['abc', 1], ['d', 456]] - headers = ['letters', 'number'] - output = tabulate_adapter.adapter(data, headers, table_format='psql') - assert output == dedent('''\ - +-----------+----------+ - | letters | number | - |-----------+----------| - | abc | 1 | - | d | 456 | - +-----------+----------+''') - - -def test_csv_wrapper(): - """Test the *output_formatter.csv_wrapper()* function.""" - # Test comma-delimited output. - data = [['abc', 1], ['d', 456]] - headers = ['letters', 'number'] - output = delimited_output_adapter.adapter(data, headers) - assert output == dedent('''\ - letters,number\r\n\ - abc,1\r\n\ - d,456\r\n''') - - # Test tab-delimited output. - data = [['abc', 1], ['d', 456]] - headers = ['letters', 'number'] - output = delimited_output_adapter.adapter( - data, headers, table_format='tsv') - assert output == dedent('''\ - letters\tnumber\r\n\ - abc\t1\r\n\ - d\t456\r\n''') - - -def test_terminal_tables_wrapper(): - """Test the *output_formatter.terminal_tables_wrapper()* function.""" - data = [['abc', 1], ['d', 456]] - headers = ['letters', 'number'] - output = terminaltables_adapter.adapter( - data, headers, table_format='ascii') - assert output == dedent('''\ - +---------+--------+ - | letters | number | - +---------+--------+ - | abc | 1 | - | d | 456 | - +---------+--------+''') - - -def test_output_formatter(): - """Test the *output_formatter.OutputFormatter* class.""" - data = [['abc', Decimal(1)], ['defg', Decimal('11.1')], - ['hi', Decimal('1.1')]] - headers = ['text', 'numeric'] - expected = dedent('''\ - +------+---------+ - | text | numeric | - +------+---------+ - | abc | 1 | - | defg | 11.1 | - | hi | 1.1 | - +------+---------+''') - - assert expected == OutputFormatter().format_output(data, headers, - format_name='ascii') diff --git a/test/test_tabulate.py b/test/test_tabulate.py deleted file mode 100644 index ae7c25ce..00000000 --- a/test/test_tabulate.py +++ /dev/null @@ -1,17 +0,0 @@ -from textwrap import dedent - -from mycli.packages import tabulate - -tabulate.PRESERVE_WHITESPACE = True - - -def test_dont_strip_leading_whitespace(): - data = [[' abc']] - headers = ['xyz'] - tbl = tabulate.tabulate(data, headers, tablefmt='psql') - assert tbl == dedent(''' - +---------+ - | xyz | - |---------| - | abc | - +---------+ ''').strip() From 02ae6b9d8d7e9c18bc87e0087b3c01e753575c15 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Mon, 1 May 2017 23:34:41 -0500 Subject: [PATCH 028/627] Add cli_helpers dependency to changelog. --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 5001405c..1e00f27e 100644 --- a/changelog.md +++ b/changelog.md @@ -22,6 +22,7 @@ Internal Changes: * Behave test source command (Thanks: [Dick Marinus]). * Test using behave the tee command (Thanks: [Dick Marinus]). * Behave fix clean up. (Thanks: [Dick Marinus]). +* Remove output formatter code in favor of CLI Helpers dependency (Thanks: [Thomas Roten]). 1.10.0: ======= From 317eae173e07af337a004a9ec4f4fc73a6e8539c Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Mon, 1 May 2017 23:44:16 -0500 Subject: [PATCH 029/627] Remove unused function. --- mycli/encodingutils.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/mycli/encodingutils.py b/mycli/encodingutils.py index 1a8b5bbb..078e6772 100644 --- a/mycli/encodingutils.py +++ b/mycli/encodingutils.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -import binascii import sys PY2 = sys.version_info[0] == 2 @@ -37,22 +36,3 @@ def utf8tounicode(arg): if PY2 and isinstance(arg, binary_type): return arg.decode('utf-8') return arg - - -def bytes_to_string(b): - """Convert bytes to a string. Hexlify bytes that can't be decoded. - - >>> print(bytes_to_string(b"\\xff")) - 0xff - >>> print(bytes_to_string('abc')) - abc - >>> print(bytes_to_string('✌')) - ✌ - - """ - if isinstance(b, binary_type): - try: - return b.decode('utf8') - except UnicodeDecodeError: - return '0x' + binascii.hexlify(b).decode('ascii') - return b From a6fb51809371ecb633591068fa5bd35efb9bfb71 Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Mon, 1 May 2017 21:33:36 +0200 Subject: [PATCH 030/627] Added a regression test to test_completion_engine.py for sqlparse >= 0.2.3 --- changelog.md | 1 + test/test_completion_engine.py | 1 + 2 files changed, 2 insertions(+) diff --git a/changelog.md b/changelog.md index 18c33271..8acc448b 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ Internal Changes: * Behave test source command (Thanks: [Dick Marinus]). * Test using behave the tee command (Thanks: [Dick Marinus]). * Behave fix clean up. (Thanks: [Dick Marinus]). +* Added a regression test for sqlparse >= 0.2.3 (Thanks: [Dick Marinus]). 1.10.0: ======= diff --git a/test/test_completion_engine.py b/test/test_completion_engine.py index 4f0406b0..42f4e2e0 100644 --- a/test/test_completion_engine.py +++ b/test/test_completion_engine.py @@ -234,6 +234,7 @@ def test_dot_col_comma_suggests_cols_or_schema_qualified_table(): 'SELECT * FROM (', 'SELECT * FROM foo WHERE EXISTS (', 'SELECT * FROM foo WHERE bar AND NOT EXISTS (', + 'SELECT 1 AS', ]) def test_sub_select_suggests_keyword(expression): suggestion = suggest_type(expression, expression) From 02629c910f6af7e7bc02660287ae8d831fe0d22c Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Mon, 1 May 2017 21:30:51 +0200 Subject: [PATCH 031/627] Revert "remove temporary hack" This reverts commit 21c600256de4c01fb4c4aa97b84ebd28f3f81c9f. --- changelog.md | 1 + mycli/packages/completion_engine.py | 44 ++++++++++++++++------------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/changelog.md b/changelog.md index 8acc448b..feedcf4d 100644 --- a/changelog.md +++ b/changelog.md @@ -12,6 +12,7 @@ Internal Changes: * Test using behave the tee command (Thanks: [Dick Marinus]). * Behave fix clean up. (Thanks: [Dick Marinus]). * Added a regression test for sqlparse >= 0.2.3 (Thanks: [Dick Marinus]). +* Reverted removal of temporary hack for sqlparse (Thanks: [Dick Marinus]). 1.10.0: ======= diff --git a/mycli/packages/completion_engine.py b/mycli/packages/completion_engine.py index b97cadf7..efbd41f6 100644 --- a/mycli/packages/completion_engine.py +++ b/mycli/packages/completion_engine.py @@ -28,28 +28,32 @@ def suggest_type(full_text, text_before_cursor): identifier = None - # If we've partially typed a word then word_before_cursor won't be an empty - # string. In that case we want to remove the partially typed string before - # sending it to the sqlparser. Otherwise the last token will always be the - # partially typed string which renders the smart completion useless because - # it will always return the list of keywords as completion. - if word_before_cursor: - if word_before_cursor.endswith( - '(') or word_before_cursor.startswith('\\'): - parsed = sqlparse.parse(text_before_cursor) - else: - parsed = sqlparse.parse( - text_before_cursor[:-len(word_before_cursor)]) + # here should be removed once sqlparse has been fixed + try: + # If we've partially typed a word then word_before_cursor won't be an empty + # string. In that case we want to remove the partially typed string before + # sending it to the sqlparser. Otherwise the last token will always be the + # partially typed string which renders the smart completion useless because + # it will always return the list of keywords as completion. + if word_before_cursor: + if word_before_cursor.endswith( + '(') or word_before_cursor.startswith('\\'): + parsed = sqlparse.parse(text_before_cursor) + else: + parsed = sqlparse.parse( + text_before_cursor[:-len(word_before_cursor)]) - # word_before_cursor may include a schema qualification, like - # "schema_name.partial_name" or "schema_name.", so parse it - # separately - p = sqlparse.parse(word_before_cursor)[0] + # word_before_cursor may include a schema qualification, like + # "schema_name.partial_name" or "schema_name.", so parse it + # separately + p = sqlparse.parse(word_before_cursor)[0] - if p.tokens and isinstance(p.tokens[0], Identifier): - identifier = p.tokens[0] - else: - parsed = sqlparse.parse(text_before_cursor) + if p.tokens and isinstance(p.tokens[0], Identifier): + identifier = p.tokens[0] + else: + parsed = sqlparse.parse(text_before_cursor) + except (TypeError, AttributeError): + return [] if len(parsed) > 1: # Multiple statements being edited -- isolate the current one by From 21cd71d0f4cf4dfb21882aa8bfca349a40809779 Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Mon, 1 May 2017 21:32:30 +0200 Subject: [PATCH 032/627] make test/test_completion_engine.py happy --- mycli/packages/completion_engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/packages/completion_engine.py b/mycli/packages/completion_engine.py index efbd41f6..88fabfa1 100644 --- a/mycli/packages/completion_engine.py +++ b/mycli/packages/completion_engine.py @@ -53,7 +53,7 @@ def suggest_type(full_text, text_before_cursor): else: parsed = sqlparse.parse(text_before_cursor) except (TypeError, AttributeError): - return [] + return [{'type': 'keyword'}] if len(parsed) > 1: # Multiple statements being edited -- isolate the current one by From 4bd32370a44713dcd266f25974d0152f4eb0619e Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Tue, 2 May 2017 09:08:04 +0200 Subject: [PATCH 033/627] move boiler plate code to before_scenario --- conftest.py | 7 ++++++ test/features/__init__.py | 0 test/features/basic_commands.feature | 16 +++----------- test/features/crud_database.feature | 8 ++----- test/features/crud_table.feature | 4 +--- test/features/environment.py | 7 ++++++ test/features/iocommands.feature | 8 ++----- test/features/named_queries.feature | 4 +--- test/features/specials.feature | 4 +--- test/features/steps/__init__.py | 0 test/features/steps/basic_commands.py | 31 +++----------------------- test/features/steps/wrappers.py | 32 +++++++++++++++++++++++++++ 12 files changed, 59 insertions(+), 62 deletions(-) create mode 100644 test/features/__init__.py create mode 100644 test/features/steps/__init__.py diff --git a/conftest.py b/conftest.py index d2cd1336..41e72ada 100644 --- a/conftest.py +++ b/conftest.py @@ -3,4 +3,11 @@ "setup.py", "mycli/magic.py", "mycli/packages/parseutils.py", + "test/features/environment.py", + "test/features/steps/basic_commands.py", + "test/features/steps/crud_database.py", + "test/features/steps/crud_table.py", + "test/features/steps/iocommands.py", + "test/features/steps/named_queries.py", + "test/features/steps/specials.py", ] diff --git a/test/features/__init__.py b/test/features/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/features/basic_commands.feature b/test/features/basic_commands.feature index 025b5850..249a68e6 100644 --- a/test/features/basic_commands.feature +++ b/test/features/basic_commands.feature @@ -2,24 +2,14 @@ Feature: run the cli, call the help command, exit the cli - Scenario: run the cli - When we run dbcli - then we see dbcli prompt - Scenario: run "\?" command - When we run dbcli - and we wait for prompt - and we send "\?" command + When we send "\?" command then we see help output Scenario: run source command - When we run dbcli - and we wait for prompt - and we send source command + When we send source command then we see help output Scenario: run the cli and exit - When we run dbcli - and we wait for prompt - and we send "ctrl + d" + When we send "ctrl + d" then dbcli exits diff --git a/test/features/crud_database.feature b/test/features/crud_database.feature index c72468c3..32d1e38d 100644 --- a/test/features/crud_database.feature +++ b/test/features/crud_database.feature @@ -2,9 +2,7 @@ Feature: manipulate databases: create, drop, connect, disconnect Scenario: create and drop temporary database - When we run dbcli - and we wait for prompt - and we create database + When we create database then we see database created when we drop database then we see database dropped @@ -12,9 +10,7 @@ Feature: manipulate databases: then we see database connected Scenario: connect and disconnect from test database - When we run dbcli - and we wait for prompt - and we connect to test database + When we connect to test database then we see database connected when we connect to dbserver then we see database connected diff --git a/test/features/crud_table.feature b/test/features/crud_table.feature index d2209fd0..bcef9292 100644 --- a/test/features/crud_table.feature +++ b/test/features/crud_table.feature @@ -2,9 +2,7 @@ Feature: manipulate tables: create, insert, update, select, delete from, drop Scenario: create, insert, select from, update, drop table - When we run dbcli - and we wait for prompt - and we connect to test database + When we connect to test database then we see database connected when we create table then we see table created diff --git a/test/features/environment.py b/test/features/environment.py index a0456b99..138f88f1 100644 --- a/test/features/environment.py +++ b/test/features/environment.py @@ -8,6 +8,8 @@ import fixture_utils as fixutils import pexpect +from steps.wrappers import run_cli, wait_prompt + def before_all(context): """Set env parameters.""" @@ -73,6 +75,11 @@ def before_step(context, _): context.atprompt = False +def before_scenario(context, _): + run_cli(context) + wait_prompt(context) + + def after_scenario(context, _): """Cleans up after each test complete.""" diff --git a/test/features/iocommands.feature b/test/features/iocommands.feature index 4bcdf6e6..38efbbb0 100644 --- a/test/features/iocommands.feature +++ b/test/features/iocommands.feature @@ -1,18 +1,14 @@ Feature: I/O commands Scenario: edit sql in file with external editor - When we run dbcli - and we wait for prompt - and we start external editor providing a file name + When we start external editor providing a file name and we type sql in the editor and we exit the editor then we see dbcli prompt and we see the sql in prompt Scenario: tee output from query - When we run dbcli - and we wait for prompt - and we tee output + When we tee output and we wait for prompt and we query "select 123456" and we wait for prompt diff --git a/test/features/named_queries.feature b/test/features/named_queries.feature index 79f31ac3..74201b92 100644 --- a/test/features/named_queries.feature +++ b/test/features/named_queries.feature @@ -2,9 +2,7 @@ Feature: named queries: save, use and delete named queries Scenario: save, use and delete named queries - When we run dbcli - and we wait for prompt - and we connect to test database + When we connect to test database then we see database connected when we save a named query then we see the named query saved diff --git a/test/features/specials.feature b/test/features/specials.feature index 9bacec45..bb367578 100644 --- a/test/features/specials.feature +++ b/test/features/specials.feature @@ -2,8 +2,6 @@ Feature: Special commands @wip Scenario: run refresh command - When we run dbcli - and we wait for prompt - and we refresh completions + When we refresh completions and we wait for prompt then we see completions refresh started diff --git a/test/features/steps/__init__.py b/test/features/steps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/features/steps/basic_commands.py b/test/features/steps/basic_commands.py index df97ee0e..299893de 100644 --- a/test/features/steps/basic_commands.py +++ b/test/features/steps/basic_commands.py @@ -7,44 +7,19 @@ """ from __future__ import unicode_literals -import pexpect -import tempfile - from behave import when +import tempfile import wrappers @when('we run dbcli') def step_run_cli(context): - """Run the process using pexpect.""" - run_args = [] - if context.conf.get('host', None): - run_args.extend(('-h', context.conf['host'])) - if context.conf.get('user', None): - run_args.extend(('-u', context.conf['user'])) - if context.conf.get('pass', None): - run_args.extend(('-p', context.conf['pass'])) - if context.conf.get('dbname', None): - run_args.extend(('-D', context.conf['dbname'])) - cli_cmd = context.conf.get('cli_command', None) or sys.executable + \ - ' -c "import coverage ; coverage.process_startup(); import mycli.main; mycli.main.cli()"' - - cmd_parts = [cli_cmd] + run_args - cmd = ' '.join(cmd_parts) - context.cli = pexpect.spawnu(cmd, cwd='..') - context.exit_sent = False - context.currentdb = context.conf['dbname'] + wrappers.run_cli(context) @when('we wait for prompt') def step_wait_prompt(context): - """Make sure prompt is displayed.""" - user = context.conf['user'] - host = context.conf['host'] - dbname = context.currentdb - wrappers.expect_exact(context, 'mysql {0}@{1}:{2}> '.format( - user, host, dbname), timeout=5) - context.atprompt = True + wrappers.wait_prompt(context) @when('we send "ctrl + d"') diff --git a/test/features/steps/wrappers.py b/test/features/steps/wrappers.py index e8d9204a..3855ede0 100644 --- a/test/features/steps/wrappers.py +++ b/test/features/steps/wrappers.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import re +import pexpect def expect_exact(context, expected, timeout): @@ -19,3 +20,34 @@ def expect_exact(context, expected, timeout): def expect_pager(context, expected, timeout): expect_exact(context, "{0}\r\n{1}{0}\r\n".format( context.conf['pager_boundary'], expected), timeout=timeout) + + +def run_cli(context): + """Run the process using pexpect.""" + run_args = [] + if context.conf.get('host', None): + run_args.extend(('-h', context.conf['host'])) + if context.conf.get('user', None): + run_args.extend(('-u', context.conf['user'])) + if context.conf.get('pass', None): + run_args.extend(('-p', context.conf['pass'])) + if context.conf.get('dbname', None): + run_args.extend(('-D', context.conf['dbname'])) + cli_cmd = context.conf.get('cli_command', None) or sys.executable + \ + ' -c "import coverage ; coverage.process_startup(); import mycli.main; mycli.main.cli()"' + + cmd_parts = [cli_cmd] + run_args + cmd = ' '.join(cmd_parts) + context.cli = pexpect.spawnu(cmd, cwd='..') + context.exit_sent = False + context.currentdb = context.conf['dbname'] + + +def wait_prompt(context): + """Make sure prompt is displayed.""" + user = context.conf['user'] + host = context.conf['host'] + dbname = context.currentdb + expect_exact(context, 'mysql {0}@{1}:{2}> '.format( + user, host, dbname), timeout=5) + context.atprompt = True From 1a24358e1c4cab3101f4353b4ca2792345e89036 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Tue, 2 May 2017 23:03:29 -0500 Subject: [PATCH 034/627] Edit last command in external editor. --- mycli/main.py | 9 +++++++-- mycli/packages/special/iocommands.py | 6 ++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 5df93408..7d81f5c5 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -419,8 +419,9 @@ def handle_editor_command(self, cli, document): saved_callables = cli.application.pre_run_callables while special.editor_command(document.text): filename = special.get_filename(document.text) - sql, message = special.open_external_editor(filename, - sql=document.text) + sql, message = special.open_external_editor( + filename, sql=document.text, + default_text=self.get_last_query() or '') if message: # Something went wrong. Raise an exception and bail. raise RuntimeError(message) @@ -756,6 +757,10 @@ def get_reserved_space(self): _, height = click.get_terminal_size() return min(round(height * reserved_space_ratio), max_reserved_space) + def get_last_query(self): + """Get the last query executed or None.""" + return self.query_history[-1][0] if self.query_history else None + @click.command() @click.option('-h', '--host', envvar='MYSQL_HOST', help='Host address of the database.') diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index 4d4fcc65..2cdc86ca 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -102,7 +102,7 @@ def get_filename(sql): return filename.strip() or None @export -def open_external_editor(filename=None, sql=''): +def open_external_editor(filename=None, sql='', default_text=''): """ Open external editor, wait for the user to type in his query, return the query. @@ -118,6 +118,8 @@ def open_external_editor(filename=None, sql=''): while pattern.search(sql): sql = pattern.sub('', sql) + text = sql if sql else default_text + message = None filename = filename.strip().split(' ', 1)[0] if filename else None @@ -125,7 +127,7 @@ def open_external_editor(filename=None, sql=''): # Populate the editor buffer with the partial sql (if available) and a # placeholder comment. - query = click.edit(sql + '\n\n' + MARKER, filename=filename, + query = click.edit(text + '\n\n' + MARKER, filename=filename, extension='.sql') if filename: From 52881034f8f77a1ae7f33be840698c9dcbc59cef Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Tue, 2 May 2017 23:04:47 -0500 Subject: [PATCH 035/627] Simplify conditional logic. --- mycli/packages/special/iocommands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index 2cdc86ca..5398d0c6 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -118,7 +118,7 @@ def open_external_editor(filename=None, sql='', default_text=''): while pattern.search(sql): sql = pattern.sub('', sql) - text = sql if sql else default_text + text = sql or default_text message = None filename = filename.strip().split(' ', 1)[0] if filename else None From eac5dbc70ada397dd1fbf8f66584725a5e811524 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Tue, 2 May 2017 23:10:50 -0500 Subject: [PATCH 036/627] Add external editor change to changelog. --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 5001405c..51a8748c 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ Features: * Handle reserved space for completion menu better in small windows. (Thanks: [Thomas Roten]). * Display current vi mode in toolbar. (Thanks: [Thomas Roten]). +* Opening an external editor will edit the last-run query. (Thanks: [Thomas Roten]). Bug Fixes: ---------- From b4a8ea41a956cd7735de1e63b88d0758758b2fb1 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Wed, 3 May 2017 12:41:00 -0500 Subject: [PATCH 037/627] Simplify editor command logic. --- mycli/main.py | 7 ++++--- mycli/packages/special/iocommands.py | 21 ++++++++++++--------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 7d81f5c5..9676161d 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -419,9 +419,10 @@ def handle_editor_command(self, cli, document): saved_callables = cli.application.pre_run_callables while special.editor_command(document.text): filename = special.get_filename(document.text) - sql, message = special.open_external_editor( - filename, sql=document.text, - default_text=self.get_last_query() or '') + query = (special.get_editor_query(document.text) or + self.get_last_query() or '') + sql, message = special.open_external_editor(filename, + sql=query) if message: # Something went wrong. Raise an exception and bail. raise RuntimeError(message) diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index 5398d0c6..abcdfa3c 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -102,13 +102,8 @@ def get_filename(sql): return filename.strip() or None @export -def open_external_editor(filename=None, sql='', default_text=''): - """ - Open external editor, wait for the user to type in his query, - return the query. - :return: list with one tuple, query as first element. - """ - +def get_editor_query(sql): + """Get the query part of an editor command.""" sql = sql.strip() # The reason we can't simply do .strip('\e') is that it strips characters, @@ -118,7 +113,15 @@ def open_external_editor(filename=None, sql='', default_text=''): while pattern.search(sql): sql = pattern.sub('', sql) - text = sql or default_text + return sql + +@export +def open_external_editor(filename=None, sql='', default_text=''): + """ + Open external editor, wait for the user to type in his query, + return the query. + :return: list with one tuple, query as first element. + """ message = None filename = filename.strip().split(' ', 1)[0] if filename else None @@ -127,7 +130,7 @@ def open_external_editor(filename=None, sql='', default_text=''): # Populate the editor buffer with the partial sql (if available) and a # placeholder comment. - query = click.edit(text + '\n\n' + MARKER, filename=filename, + query = click.edit(sql + '\n\n' + MARKER, filename=filename, extension='.sql') if filename: From 541929ff7ed2cb8c8c2903694f547c20bd2130dc Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Wed, 3 May 2017 12:43:24 -0500 Subject: [PATCH 038/627] Remove unused argument. --- mycli/packages/special/iocommands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index abcdfa3c..8472792b 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -116,7 +116,7 @@ def get_editor_query(sql): return sql @export -def open_external_editor(filename=None, sql='', default_text=''): +def open_external_editor(filename=None, sql=''): """ Open external editor, wait for the user to type in his query, return the query. From b5cd5f31f4cac078e2050cf8524b58393a57a1a4 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Wed, 3 May 2017 12:44:19 -0500 Subject: [PATCH 039/627] Pep8 fix. --- mycli/packages/special/iocommands.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index 8472792b..0e09a9ec 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -117,9 +117,9 @@ def get_editor_query(sql): @export def open_external_editor(filename=None, sql=''): - """ - Open external editor, wait for the user to type in his query, - return the query. + """Open external editor, wait for the user to type in their query, return + the query. + :return: list with one tuple, query as first element. """ From 466d0f3ea01d7f83e9c5470e9b61221d4a2c7165 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Wed, 3 May 2017 12:48:40 -0500 Subject: [PATCH 040/627] Simplify editing logic. --- mycli/main.py | 2 +- mycli/packages/special/iocommands.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 9676161d..af535ca3 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -420,7 +420,7 @@ def handle_editor_command(self, cli, document): while special.editor_command(document.text): filename = special.get_filename(document.text) query = (special.get_editor_query(document.text) or - self.get_last_query() or '') + self.get_last_query()) sql, message = special.open_external_editor(filename, sql=query) if message: diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index 0e09a9ec..29a9f99e 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -116,7 +116,7 @@ def get_editor_query(sql): return sql @export -def open_external_editor(filename=None, sql=''): +def open_external_editor(filename=None, sql=None): """Open external editor, wait for the user to type in their query, return the query. @@ -126,12 +126,13 @@ def open_external_editor(filename=None, sql=''): message = None filename = filename.strip().split(' ', 1)[0] if filename else None + sql = sql or '' MARKER = '# Type your query above this line.\n' # Populate the editor buffer with the partial sql (if available) and a # placeholder comment. - query = click.edit(sql + '\n\n' + MARKER, filename=filename, - extension='.sql') + query = click.edit("{sql}\n\n{marker}".format(sql=sql, marker=MARKER), + filename=filename, extension='.sql') if filename: try: From b68c3b2b21730cf69da24f1cd8ad9a8886b5ea56 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Wed, 3 May 2017 12:49:36 -0500 Subject: [PATCH 041/627] Single quotes instead of double. --- mycli/packages/special/iocommands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index 29a9f99e..87d68273 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -131,7 +131,7 @@ def open_external_editor(filename=None, sql=None): # Populate the editor buffer with the partial sql (if available) and a # placeholder comment. - query = click.edit("{sql}\n\n{marker}".format(sql=sql, marker=MARKER), + query = click.edit('{sql}\n\n{marker}'.format(sql=sql, marker=MARKER), filename=filename, extension='.sql') if filename: From f34c042075dd1df10c32a69d7e5460f0409e2e3a Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Wed, 3 May 2017 12:50:15 -0500 Subject: [PATCH 042/627] Extra newlines for PEP 8. --- mycli/packages/special/iocommands.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index 87d68273..19c2dc73 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -101,6 +101,7 @@ def get_filename(sql): command, _, filename = sql.partition(' ') return filename.strip() or None + @export def get_editor_query(sql): """Get the query part of an editor command.""" @@ -115,6 +116,7 @@ def get_editor_query(sql): return sql + @export def open_external_editor(filename=None, sql=None): """Open external editor, wait for the user to type in their query, return From 133e197eb1e3af2e339c07039e4839496fce1d8f Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Wed, 3 May 2017 12:56:14 -0500 Subject: [PATCH 043/627] Last pep8 fix :) --- mycli/main.py | 3 +-- mycli/packages/special/iocommands.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index af535ca3..2e2913d0 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -421,8 +421,7 @@ def handle_editor_command(self, cli, document): filename = special.get_filename(document.text) query = (special.get_editor_query(document.text) or self.get_last_query()) - sql, message = special.open_external_editor(filename, - sql=query) + sql, message = special.open_external_editor(filename, sql=query) if message: # Something went wrong. Raise an exception and bail. raise RuntimeError(message) diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index 19c2dc73..92773f8d 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -123,6 +123,7 @@ def open_external_editor(filename=None, sql=None): the query. :return: list with one tuple, query as first element. + """ message = None From e8bbe5eb30c683c99d359bfd4c7425c98a065b2a Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 07:34:04 -0500 Subject: [PATCH 044/627] Add docstring and use io.open. --- release.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/release.py b/release.py index 81769583..f75277f0 100755 --- a/release.py +++ b/release.py @@ -1,7 +1,10 @@ #!/usr/bin/env python +"""A script to publish a release of mycli to PyPI.""" + from __future__ import print_function import re import ast +import io import subprocess import sys from optparse import OptionParser @@ -51,9 +54,8 @@ def run_step(*args): def version(version_file): _version_re = re.compile(r'__version__\s+=\s+(.*)') - with open(version_file, 'rb') as f: - ver = str(ast.literal_eval(_version_re.search( - f.read().decode('utf-8')).group(1))) + with io.open(version_file, encoding='utf-8') as f: + ver = str(ast.literal_eval(_version_re.search(f.read()).group(1))) return ver @@ -61,7 +63,8 @@ def version(version_file): def commit_for_release(version_file, ver): run_step('git', 'reset') run_step('git', 'add', version_file) - run_step('git', 'commit', '--message', 'Releasing version %s' % ver) + run_step('git', 'commit', '--message', + 'Releasing version {}'.format(ver)) def create_git_tag(tag_name): @@ -128,7 +131,7 @@ def checklist(questions): sys.exit(1) commit_for_release('mycli/__init__.py', ver) - create_git_tag('v%s' % ver) + create_git_tag('v{}'.format(ver)) register_with_pypi() create_distribution_files() push_to_github() From 3cd6492b6105a0babdf45ca0b58d09745fcf2e00 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 07:36:46 -0500 Subject: [PATCH 045/627] Don't evaluate Python code. --- release.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/release.py b/release.py index f75277f0..02d0e1f5 100755 --- a/release.py +++ b/release.py @@ -2,9 +2,8 @@ """A script to publish a release of mycli to PyPI.""" from __future__ import print_function -import re -import ast import io +import re import subprocess import sys from optparse import OptionParser @@ -52,10 +51,11 @@ def run_step(*args): def version(version_file): - _version_re = re.compile(r'__version__\s+=\s+(.*)') + _version_re = re.compile( + r'__version__\s+=\s+(?P[\'"])(?P.*)(?P=quote)') with io.open(version_file, encoding='utf-8') as f: - ver = str(ast.literal_eval(_version_re.search(f.read()).group(1))) + ver = _version_re.search(f.read()).group('version') return ver From f7194c6b19b57879ebee6abac6a7ab22b553680f Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 09:29:02 -0500 Subject: [PATCH 046/627] Use click's confirm for release script. --- release.py | 17 +++++------------ requirements-dev.txt | 1 + 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/release.py b/release.py index 02d0e1f5..499b9cb0 100755 --- a/release.py +++ b/release.py @@ -3,15 +3,12 @@ from __future__ import print_function import io +from optparse import OptionParser import re import subprocess import sys -from optparse import OptionParser -try: - input = raw_input -except NameError: - pass +import click DEBUG = False CONFIRM_STEPS = False @@ -26,9 +23,7 @@ def skip_step(): global CONFIRM_STEPS if CONFIRM_STEPS: - choice = input("--- Confirm step? (y/N) [y] ") - if choice.lower() == 'n': - return True + return not click.confirm('--- Run this step?', default=True) return False @@ -93,8 +88,7 @@ def push_tags_to_github(): def checklist(questions): for question in questions: - choice = input(question + ' (y/N) [n] ') - if choice.lower() != 'y': + if not click.confirm('--- {}'.format(question), default=False): sys.exit(1) @@ -126,8 +120,7 @@ def checklist(questions): CONFIRM_STEPS = popts.confirm_steps DRY_RUN = popts.dry_run - choice = input('Are you sure? (y/N) [n] ') - if choice.lower() != 'y': + if not click.confirm('Are you sure?', default=False): sys.exit(1) commit_for_release('mycli/__init__.py', ver) diff --git a/requirements-dev.txt b/requirements-dev.txt index b7e6e2dc..cf552f48 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,3 +6,4 @@ behave pexpect coverage==4.3.4 pep8radius +click==6.7 From 945744537348a185b9bfe6a7ad7ad0d2237e2153 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 09:30:19 -0500 Subject: [PATCH 047/627] Remove unneeded register step. --- release.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/release.py b/release.py index 499b9cb0..18e1b8f2 100755 --- a/release.py +++ b/release.py @@ -66,10 +66,6 @@ def create_git_tag(tag_name): run_step('git', 'tag', tag_name) -def register_with_pypi(): - run_step('python', 'setup.py', 'register') - - def create_distribution_files(): run_step('python', 'setup.py', 'sdist', 'bdist_wheel') @@ -125,7 +121,6 @@ def checklist(questions): commit_for_release('mycli/__init__.py', ver) create_git_tag('v{}'.format(ver)) - register_with_pypi() create_distribution_files() push_to_github() push_tags_to_github() From 1f1c9369d1fb862d6120ae3540205ff78fb996bd Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 09:47:22 -0500 Subject: [PATCH 048/627] Pin the correct version of pep8radius for development. --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index b7e6e2dc..19df802e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,4 +5,4 @@ twine==1.8.1 behave pexpect coverage==4.3.4 -pep8radius +git+https://github.com/hayd/pep8radius.git@c8aebd0e1d272160896124e104773b97a6249c3e#egg=pep8radius From c27a2939002aad4c65da75279b46935a10a33fc7 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 10:18:31 -0500 Subject: [PATCH 049/627] Add Makefile. --- Makefile | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ad6cfbbc --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +.PHONY: clean lint lint-fix test test-all + +help: + @echo "clean - remove all build artifacts" + @echo "lint - check code changes against PEP 8" + @echo "lint-fix - automatically fix PEP 8 violations" + @echo "test - run tests quickly with the current Python" + @echo "test-all - run tests in all environments" + +clean: + rm -rf build dist egg *.egg-info + find . -name '*.py[co]' -exec rm -f {} + + +lint: + pep8radius master --docformatter --error-status || ( pep8radius master --docformatter --diff; false ) + +lint-fix: + pep8radius master --docformatter --in-place + +test: + if pytest ; then cd test && behave ; fi + +test-all: + tox From a0c38677e3323773129466dd3499633d7cd219e1 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 10:22:03 -0500 Subject: [PATCH 050/627] Add Makefile to manifest. --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index a50b7368..f787b945 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ -include LICENSE.txt *.md *.rst TODO requirements-dev.txt screenshots/* +include LICENSE.txt *.md *.rst TODO requirements-dev.txt Makefile screenshots/* include conftest.py .coveragerc pytest.ini test tox.ini From 469e0adbb144419da9d89bfd2475b8874b1955e0 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 10:22:43 -0500 Subject: [PATCH 051/627] Add Makefile to changelog. --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 1e9fd5fd..ad9edfb1 100644 --- a/changelog.md +++ b/changelog.md @@ -27,6 +27,7 @@ Internal Changes: * Better handle common before/after scenarios in behave. (Thanks: [Dick Marinus]) * Added a regression test for sqlparse >= 0.2.3 (Thanks: [Dick Marinus]). * Reverted removal of temporary hack for sqlparse (Thanks: [Dick Marinus]). +* Add Makefile to simplify development tasks (Thanks: [Thomas Roten]). 1.10.0: ======= From 5f75dc18942f62f72a7dbfe94270c43fa3a8f775 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 14:13:57 -0500 Subject: [PATCH 052/627] Streamline development guide using make commands. --- DEVELOP.rst | 99 ++++++++++++++++++++++++----------------------------- 1 file changed, 44 insertions(+), 55 deletions(-) diff --git a/DEVELOP.rst b/DEVELOP.rst index 0d53e6ad..c4699f62 100644 --- a/DEVELOP.rst +++ b/DEVELOP.rst @@ -1,86 +1,75 @@ Development Guide ----------------- + This is a guide for developers who would like to contribute to this project. +If you're interested in contributing to mycli, thank you. We'd love your help! +You'll always get credit for your work. + GitHub Workflow --------------- -If you're interested in contributing to mycli, first of all my heart felt -thanks. `Fork the project `_ in github. Then -clone your fork into your computer (``git clone ``). Make -the changes and create the commits in your local machine. Then push those -changes to your fork. Then click on the pull request icon on github and create -a new pull request. Add a description about the change and send it along. I -promise to review the pull request in a reasonable window of time and get back -to you. +1. `Fork the repository `_ on GitHub. +2. Clone your fork locally:: -In order to keep your fork up to date with any changes from mainline, add a new -git remote to your local copy called 'upstream' and point it to the main mycli -repo. + $ git clone -:: +3. Add the official repository (``upstream``) as a remote repository:: - $ git remote add upstream git@github.com:dbcli/mycli.git + $ git remote add upstream git@github.com:dbcli/mycli.git -Once the 'upstream' end point is added you can then periodically do a ``git -pull upstream master`` to update your local copy and then do a ``git push -origin master`` to keep your own fork up to date. +4. Set up a `virtual environment `_ + for development:: -Local Setup ------------ + $ cd mycli + $ pip install virtualenv + $ virtualenv mycli_dev -The installation instructions in the README file are intended for users of -mycli. If you're developing mycli, you'll need to install it in a slightly -different way so you can see the effects of your changes right away without -having to go through the install cycle everytime you change the code. + We've just created a virtual environment that we'll use to install all the dependencies + and tools we need to work on mycli. Whenever you want to work on mycli, you + need to activate the virtual environment:: -It is highly recommended to use virtualenv for development. If you don't know -what a virtualenv is, this `guide `_ -will help you get started. + $ source mycli_dev/bin/activate -Create a virtualenv (let's call it mycli-dev). Activate it: +5. Install the dependencies and development tools:: -:: + $ pip install -r requirements-dev.txt + $ pip install --editable . - source ./mycli-dev/bin/activate +6. Create a branch for your bugfix or feature:: -Once the virtualenv is activated, `cd` into the local clone of mycli folder -and install mycli using pip as follows: + $ git checkout -b -:: +7. While you work on your bugfix or feature, be sure to pull the latest changes from ``upstream``. This ensures that your local codebase is up-to-date:: - $ pip install --editable . + $ git pull upstream master + + +Running the Tests +----------------- - or +While you work on mycli, it's important to run the tests to make sure your code +hasn't broken any existing functionality. To run the tests, just type in:: - $ pip install -e . + $ make test -This will install the necessary dependencies as well as install mycli from the -working folder into the virtualenv. By installing it using `pip install -e` -we've linked the mycli installation with the working copy. So any changes made -to the code is immediately available in the installed version of mycli. This -makes it easy to change something in the code, launch mycli and check the -effects of your change. +Mycli supports Python 2.7 and 3.3+. You can test against multiple versions of +Python by running:: -Building DEB package from scratch --------------------- + $ make test-all -First pip install `make-deb`. Then run make-deb. It will create a debian folder -after asking a few questions like maintainer name, email etc. -$ vagrant up +Coding Style +------------ -PEP8 checks ------------ +Mycli requires code submissions to adhere to +`PEP 8 `_. +It's easy to check the style of your code, just run:: -When you submit a PR, the changeset is checked for pep8 compliance using -`pep8radius `_. If you see a build failing because -of these checks, install pep8radius and apply style fixes: + $ make lint -:: +If you see any PEP 8 style issues, you can automatically fix them by running:: - $ pip install pep8radius - $ pep8radius --docformatter --diff # view a diff of proposed fixes - $ pep8radius --docformatter --in-place # apply the fixes + $ make lint-fix -Then commit and push the fixes. +Be sure to commit and push any PEP 8 fixes. From 615564c34b52b022816d64fe22f254834b099c19 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 14:18:43 -0500 Subject: [PATCH 053/627] Remove outdated release procedure file. --- release_procedure.txt | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 release_procedure.txt diff --git a/release_procedure.txt b/release_procedure.txt deleted file mode 100644 index 1b935b62..00000000 --- a/release_procedure.txt +++ /dev/null @@ -1,14 +0,0 @@ -# vi: ft=vimwiki - -* Bump the version number in mycli/__init__.py -* Commit with message: 'Releasing version X.X.X.' -* Create a tag: git tag vX.X.X -* Register with pypi for new version: python setup.py register -* Fix the image url in PyPI to point to github raw content. https://raw.githubusercontent.com/dbcli/mysql-cli/master/screenshots/image01.png -* Create source dist tar ball: python setup.py sdist -* Test this by installing it in a fresh new virtualenv. Run SanityChecks [./sanity_checks.txt]. -* Upload the source dist to PyPI: https://pypi.python.org/pypi/mycli -* pip install mycli -* Run SanityChecks. -* Push the version back to github: git push --tags origin master -* Done! From 593e68b0d60efc0ead15e782e04594e992b8a245 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 21:09:01 -0500 Subject: [PATCH 054/627] Remove unused files. --- MANIFEST.in | 2 +- TODO | 10 ------ Vagrantfile | 30 ------------------ create_deb.sh | 31 ------------------ debian/changelog | 74 ------------------------------------------- debian/compat | 1 - debian/control | 13 -------- debian/mycli.triggers | 8 ----- debian/postinst | 5 --- debian/postrm | 5 --- debian/rules | 4 --- release_procedure.txt | 14 -------- 12 files changed, 1 insertion(+), 196 deletions(-) delete mode 100644 TODO delete mode 100644 Vagrantfile delete mode 100755 create_deb.sh delete mode 100644 debian/changelog delete mode 100644 debian/compat delete mode 100644 debian/control delete mode 100644 debian/mycli.triggers delete mode 100644 debian/postinst delete mode 100644 debian/postrm delete mode 100644 debian/rules delete mode 100644 release_procedure.txt diff --git a/MANIFEST.in b/MANIFEST.in index a50b7368..d9bc0186 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ -include LICENSE.txt *.md *.rst TODO requirements-dev.txt screenshots/* +include LICENSE.txt *.md *.rst requirements-dev.txt screenshots/* include conftest.py .coveragerc pytest.ini test tox.ini diff --git a/TODO b/TODO deleted file mode 100644 index 64c9842a..00000000 --- a/TODO +++ /dev/null @@ -1,10 +0,0 @@ -# vi: ft=vimwiki - -* [ ] Check if views are available in mysql. -* [ ] Create waffle.io page. -* [ ] Setup gitter. -* [ ] Setup a landing page for mycli.net. -* [ ] Send out invites to backers, pgcli contributors. -* [ ] Write a blog post on personal blog about the experience of kickstarter. -* [ ] Check mycli against MariaDB and Percona. -* [ ] Use error codes instead of matching error strings for reconnect, auto-password prompt etc. diff --git a/Vagrantfile b/Vagrantfile deleted file mode 100644 index a514d1ef..00000000 --- a/Vagrantfile +++ /dev/null @@ -1,30 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -Vagrant.configure(2) do |config| - - config.vm.synced_folder ".", "/mycli" - - config.vm.define "debian" do |debian| - debian.vm.box = "debian/jessie64" - debian.vm.provision "shell", inline: <<-SHELL - echo "-> Building DEB" - sudo apt-get update - sudo echo "deb http://ppa.launchpad.net/spotify-jyrki/dh-virtualenv/ubuntu trusty main" >> /etc/apt/sources.list - sudo echo "deb-src http://ppa.launchpad.net/spotify-jyrki/dh-virtualenv/ubuntu trusty main" >> /etc/apt/sources.list - sudo apt-get update - sudo apt-get install -y --force-yes python-virtualenv dh-virtualenv debhelper build-essential python-setuptools python-dev - echo "-> Cleaning up old workspace" - rm -rf build - mkdir -p build - cp -r /mycli build/. - cd build/mycli - - echo "-> Creating mycli deb" - dpkg-buildpackage -us -uc - cp ../*.deb /mycli/. - SHELL - end - -end - diff --git a/create_deb.sh b/create_deb.sh deleted file mode 100755 index 8e75ebb3..00000000 --- a/create_deb.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/sh - -set -e - -make-deb -cd debian - -cat > postinst <<- EOM -#!/bin/bash - -echo "Setting up symlink to mycli" -ln -sf /usr/share/python/mycli/bin/mycli /usr/local/bin/mycli -EOM -echo "Created postinst file." - -cat > postrm <<- EOM -#!/bin/bash - -echo "Removing symlink to mycli" -rm /usr/local/bin/mycli -EOM -echo "Created postrm file." - -for f in * -do - echo "" >> $f; -done - -echo "INFO: debian folder is setup and ready." -echo "INFO: 1. Update the changelog with real changes." -echo "INFO: 2. Run:\n\tvagrant provision || vagrant up" diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index 5135bf68..00000000 --- a/debian/changelog +++ /dev/null @@ -1,74 +0,0 @@ -mycli (1.7.0) unstable; urgency=medium - - * Add stdin batch mode. (Thanks: Thomas Roten). - * Add warn/no-warn command-line options. (Thanks: Thomas Roten). - * Upgrade sqlparse dependency to 0.1.19. (Thanks: [Amjith Ramanujam]). - * Update features list in README.md. (Thanks: Matheus Rosa). - * Remove extra \n in features list in README.md. (Thanks: Matheus Rosa). - * Enable history search via . (Thanks: [Amjith Ramanujam]). - * Upgrade prompt_toolkit to 1.0.0. (Thanks: Jonathan Slenders) - - -- Casper Langemeijer Fri, 27 May 2016 12:03:31 +0200 - -mycli (1.6.0) unstable; urgency=medium - - * Change continuation prompt for multi-line mode to match default mysql. - * Add status command to match mysql's status command. (Thanks: Thomas Roten). - * Add SSL support for mycli. (Thanks: Artem Bezsmertnyi). - * Add auto-completion and highlight support for OFFSET keyword. (Thanks: Matheus Rosa). - * Add support for MYSQL_TEST_LOGIN_FILE env variable to specify alternate login file. (Thanks: Thomas Roten). - * Add support for --auto-vertical-output to automatically switch to vertical output if the output doesn't fit in the table format. - * Add support for system-wide config. Now /etc/myclirc will be honored. (Thanks: Thomas Roten). - * Add support for nopager and \n to turn off the pager. (Thanks: Thomas Roten). - * Add support for --local-infile command-line option. (Thanks: Thomas Roten). - * Remove -S from less option which was clobbering the scroll back in history. (Thanks: Thomas Roten). - * Make system command work with Python 3. (Thanks: Thomas Roten). - * Support \G terminator for \f queries. (Thanks: Terseus). - * Upgrade prompt_toolkit to 0.60. - * Add Python 3.5 to test environments. (Thanks: Thomas Roten). - * Remove license meta-data. (Thanks: Thomas Roten). - * Skip binary tests if PyMySQL version does not support it. (Thanks: Thomas Roten). - * Refactor pager handling. (Thanks: Thomas Roten) - * Capture warnings to log file. (Thanks: Mikhail Borisov). - * Make syntax_style a tiny bit more intuitive. (Thanks: Phil Cohen). - - -- Casper Langemeijer Fri, 27 May 2016 12:03:31 +0200 - -mycli (1.5.2) unstable; urgency=low - - * Protect against port number being None when no port is specified in command line. - * Cast the value of port read from my.cnf to int. - * Make a config option to enable `audit_log`. (Thanks: [Matheus Rosa]). - * Add support for reading .mylogin.cnf to get user credentials. (Thanks: [Thomas Roten]). - * Register the special command `prompt` with the `\R` as alias. (Thanks: [Matheus Rosa]). - * Perform completion refresh in a background thread. Now mycli can handle - * Add support for `system` command. (Thanks: [Matheus Rosa]). - * Caught and hexed binary fields in MySQL. (Thanks: [Daniel West]). - * Treat enter key as tab when the suggestion menu is open. (Thanks: [Matheus Rosa]) - * Add "delete" and "truncate" as destructive commands. (Thanks: [Martijn Engler]). - * Change \dt syntax to add an optional table name. (Thanks: [Shoma Suzuki]). - * Add TRANSACTION related keywords. - * Treat DESC and EXPLAIN as DESCRIBE. (Thanks: [spacewander]). - * Fix the removal of whitespace from table output. - * Add ability to make suggestions for compound join clauses. (Thanks: [Matheus Rosa]). - * Fix the incorrect reporting of command time. - * Add type validation for port argument. (Thanks [Matheus Rosa]) - * Make pycrypto optional and only install it in \*nix systems. (Thanks: [Iryna Cherniavska]). - * Add badge for PyPI version to README. (Thanks: [Shoma Suzuki]). - * Updated release script with a --dry-run and --confirm-steps option. (Thanks: [Iryna Cherniavska]). - * Adds support for PyMySQL 0.6.2 and above. This is useful for debian package builders. (Thanks: [Thomas Roten]). - * Disable click warning. - - -- Casper Langemeijer Sun, 15 Nov 2015 10:26:24 +0100 - -mycli (1.4.0) unstable; urgency=low - - * Add `source` command. This allows running sql statement from a file. - * Added a config option to make the warning before destructive commands optional. (Thanks: [Daniel West](https://github.com/danieljwest)) - * Add completion support for CHANGE TO and other master/slave commands. This is still preliminary and it will be enhanced in the future. - * Add custom styles to color the menus and toolbars. - * Upgrade prompt_toolkit to 0.46. (Thanks: [Jonathan Slenders](https://github.com/jonathanslenders)) - * Fix keyword completion after the `WHERE` clause. - * Add `\g` and `\G` as valid query terminators. Previously in multi-line mode ending a query with a `\G` wouldn't run the query. This is now fixed. - - -- Amjith Ramanujam Sun, 23 Aug 2015 20:14:45 +0000 diff --git a/debian/compat b/debian/compat deleted file mode 100644 index ec635144..00000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -9 diff --git a/debian/control b/debian/control deleted file mode 100644 index 41838326..00000000 --- a/debian/control +++ /dev/null @@ -1,13 +0,0 @@ -Source: mycli -Section: python -Priority: extra -Maintainer: Amjith Ramanujam -Build-Depends: debhelper (>= 9), python, dh-virtualenv (>= 0.7), python-setuptools, python-dev -Standards-Version: 3.9.5 - -Package: mycli -Architecture: any -Pre-Depends: dpkg (>= 1.16.1), python2.7-minimal, ${misc:Pre-Depends} -Depends: ${python:Depends}, ${misc:Depends} -Description: CLI for MySQL Database. With auto-completion and syntax highlighting. - CLI for MySQL Database. With auto-completion and syntax highlighting. diff --git a/debian/mycli.triggers b/debian/mycli.triggers deleted file mode 100644 index b0b1d218..00000000 --- a/debian/mycli.triggers +++ /dev/null @@ -1,8 +0,0 @@ -# Register interest in Python interpreter changes (Python 2 for now); and -# don't make the Python package dependent on the virtualenv package -# processing (noawait) -interest-noawait /usr/bin/python2.7 - -# Also provide a symbolic trigger for all dh-virtualenv packages -interest dh-virtualenv-interpreter-update - diff --git a/debian/postinst b/debian/postinst deleted file mode 100644 index 122ff285..00000000 --- a/debian/postinst +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -echo "Setting up symlink to mycli" -ln -sf /usr/share/python/mycli/bin/mycli /usr/local/bin/mycli - diff --git a/debian/postrm b/debian/postrm deleted file mode 100644 index 850d6e8d..00000000 --- a/debian/postrm +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -echo "Removing symlink to mycli" -rm /usr/local/bin/mycli - diff --git a/debian/rules b/debian/rules deleted file mode 100644 index 299e3091..00000000 --- a/debian/rules +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/make -f - -%: - dh $@ --with python-virtualenv diff --git a/release_procedure.txt b/release_procedure.txt deleted file mode 100644 index 1b935b62..00000000 --- a/release_procedure.txt +++ /dev/null @@ -1,14 +0,0 @@ -# vi: ft=vimwiki - -* Bump the version number in mycli/__init__.py -* Commit with message: 'Releasing version X.X.X.' -* Create a tag: git tag vX.X.X -* Register with pypi for new version: python setup.py register -* Fix the image url in PyPI to point to github raw content. https://raw.githubusercontent.com/dbcli/mysql-cli/master/screenshots/image01.png -* Create source dist tar ball: python setup.py sdist -* Test this by installing it in a fresh new virtualenv. Run SanityChecks [./sanity_checks.txt]. -* Upload the source dist to PyPI: https://pypi.python.org/pypi/mycli -* pip install mycli -* Run SanityChecks. -* Push the version back to github: git push --tags origin master -* Done! From 1136a1f88c4fe333348b9031341fc5ed2acbbcc5 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 21:13:27 -0500 Subject: [PATCH 055/627] Move coverage config to setup.cfg. --- .coveragerc | 3 --- setup.cfg | 4 ++++ 2 files changed, 4 insertions(+), 3 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index ae818eef..00000000 --- a/.coveragerc +++ /dev/null @@ -1,3 +0,0 @@ -[run] -parallel=True -source=mycli diff --git a/setup.cfg b/setup.cfg index 2a9acf13..c017a54d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,6 @@ [bdist_wheel] universal = 1 + +[coverage:run] +parallel=True +source=mycli From ffd74e6979acc81c5e21957716d740dc3643e343 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 21:14:36 -0500 Subject: [PATCH 056/627] Move pytest config to setup.cfg. --- pytest.ini | 2 -- setup.cfg | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) delete mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 7c5b52b7..00000000 --- a/pytest.ini +++ /dev/null @@ -1,2 +0,0 @@ -[pytest] -addopts=--capture=sys --showlocals --doctest-modules diff --git a/setup.cfg b/setup.cfg index c017a54d..b92934a4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,3 +4,6 @@ universal = 1 [coverage:run] parallel=True source=mycli + +[pytest] +addopts=--capture=sys --showlocals --doctest-modules From ead3a32ae7c370b834e3d7de1e91bcd1f1db9d59 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 21:19:11 -0500 Subject: [PATCH 057/627] Update pytest section to tool:pytest. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index b92934a4..994fc8d8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,5 +5,5 @@ universal = 1 parallel=True source=mycli -[pytest] +[tool:pytest] addopts=--capture=sys --showlocals --doctest-modules From ff7bc198ee6c9311bcf8451654c64992a6d8393c Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 22:11:07 -0500 Subject: [PATCH 058/627] Move conftest.py into setup.cfg. --- conftest.py | 13 ------------- setup.cfg | 8 +++++++- 2 files changed, 7 insertions(+), 14 deletions(-) delete mode 100644 conftest.py diff --git a/conftest.py b/conftest.py deleted file mode 100644 index 41e72ada..00000000 --- a/conftest.py +++ /dev/null @@ -1,13 +0,0 @@ -import sys -collect_ignore = [ - "setup.py", - "mycli/magic.py", - "mycli/packages/parseutils.py", - "test/features/environment.py", - "test/features/steps/basic_commands.py", - "test/features/steps/crud_database.py", - "test/features/steps/crud_table.py", - "test/features/steps/iocommands.py", - "test/features/steps/named_queries.py", - "test/features/steps/specials.py", -] diff --git a/setup.cfg b/setup.cfg index 994fc8d8..0854bbe4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,4 +6,10 @@ parallel=True source=mycli [tool:pytest] -addopts=--capture=sys --showlocals --doctest-modules +addopts = --capture=sys + --showlocals + --doctest-modules + --ignore=setup.py + --ignore=mycli/magic.py + --ignore=mycli/packages/parseutils.py + --ignore=test/features From 0a4675dd65e1b6379d1505379e2e8a776a772eb5 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 22:11:24 -0500 Subject: [PATCH 059/627] Standardize spacing in setup.cfg. --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 0854bbe4..041a857b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,8 +2,8 @@ universal = 1 [coverage:run] -parallel=True -source=mycli +parallel = True +source = mycli [tool:pytest] addopts = --capture=sys From befb8a02dd63b98ba2558a8213244a41cc07c75d Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 22:21:08 -0500 Subject: [PATCH 060/627] Use setup.cfg for coverage subprocess tests. --- test/features/environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/features/environment.py b/test/features/environment.py index 138f88f1..0caf4793 100644 --- a/test/features/environment.py +++ b/test/features/environment.py @@ -16,7 +16,7 @@ def before_all(context): os.environ['LINES'] = "100" os.environ['COLUMNS'] = "100" os.environ['EDITOR'] = 'ex' - os.environ["COVERAGE_PROCESS_START"] = os.getcwd() + "/../.coveragerc" + os.environ["COVERAGE_PROCESS_START"] = os.getcwd() + "/../setup.cfg" context.exit_sent = False From e9179a7a8a2d0ff9b24ce019251d5f571d2cf990 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 23:12:31 -0500 Subject: [PATCH 061/627] Use pytest-cov plugin. --- .travis.yml | 5 +++-- requirements-dev.txt | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index de590fc0..5e2dea07 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,12 +7,13 @@ python: - "3.6" install: - - pip install PyMySQL . pytest mock codecov pexpect behave + - pip install PyMySQL pytest mock codecov pexpect behave pytest-cov - pip install git+https://github.com/hayd/pep8radius.git + - pip install -e . script: - set -e - - coverage run --source mycli -m py.test + - pytest --cov-report= --cov=mycli - cd test - behave - cd .. diff --git a/requirements-dev.txt b/requirements-dev.txt index cf552f48..c7e7c2c4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,6 @@ mock pytest +pytest-cov==2.4.0 tox twine==1.8.1 behave From ec4fad0716952c15a6e081399ea49f23ead83849 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 23:21:38 -0500 Subject: [PATCH 062/627] Travis linter says to drop on_start. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5e2dea07..7a1023d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,4 +31,3 @@ notifications: - YOUR_WEBHOOK_URL on_success: change # options: [always|never|change] default: always on_failure: always # options: [always|never|change] default: always - on_start: false # default: false From 3065c0d2bf72c57ca373c800e697324f84e6c4ab Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sat, 6 May 2017 23:25:20 -0500 Subject: [PATCH 063/627] pytest isn't found on <3.4 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7a1023d5..c68e6a9b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ install: script: - set -e - - pytest --cov-report= --cov=mycli + - py.test --cov-report= --cov=mycli - cd test - behave - cd .. From d06ccd48ba0f2c51a262ba8f2e57f3eaab391fdb Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sun, 7 May 2017 00:02:39 -0500 Subject: [PATCH 064/627] Make behave work from project root as well. --- .travis.yml | 4 +--- setup.cfg | 2 ++ test/features/environment.py | 9 +++++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index c68e6a9b..49ec67d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,9 +14,7 @@ install: script: - set -e - py.test --cov-report= --cov=mycli - - cd test - - behave - - cd .. + - behave test/features # check for pep8 errors, only looking at branch vs master. If there are errors, show diff and return an error code. - pep8radius master --docformatter --error-status || ( pep8radius master --docformatter --diff; false ) - set +e diff --git a/setup.cfg b/setup.cfg index 041a857b..b7286a9b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,6 +9,8 @@ source = mycli addopts = --capture=sys --showlocals --doctest-modules + --cov-report= + --cov=mycli --ignore=setup.py --ignore=mycli/magic.py --ignore=mycli/packages/parseutils.py diff --git a/test/features/environment.py b/test/features/environment.py index 0caf4793..42ed8df1 100644 --- a/test/features/environment.py +++ b/test/features/environment.py @@ -10,13 +10,16 @@ from steps.wrappers import run_cli, wait_prompt +PACKAGE_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + def before_all(context): """Set env parameters.""" os.environ['LINES'] = "100" os.environ['COLUMNS'] = "100" os.environ['EDITOR'] = 'ex' - os.environ["COVERAGE_PROCESS_START"] = os.getcwd() + "/../setup.cfg" + os.environ["COVERAGE_PROCESS_START"] = os.path.join(PACKAGE_ROOT, + 'setup.cfg') context.exit_sent = False @@ -48,7 +51,9 @@ def before_all(context): 'pager_boundary': '---boundary---', } os.environ['PAGER'] = "{0} {1} {2}".format( - sys.executable, "test/features/wrappager.py", context.conf['pager_boundary']) + sys.executable, + os.path.join(PACKAGE_ROOT, '/test/features/wrappager.py'), + context.conf['pager_boundary']) context.cn = dbutils.create_db(context.conf['host'], context.conf['user'], context.conf['pass'], From 88d001ed77935cf2d44fb6f5524a51edfe55e6f8 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sun, 7 May 2017 00:03:43 -0500 Subject: [PATCH 065/627] Don't run coverage for every py.test. --- setup.cfg | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index b7286a9b..041a857b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,8 +9,6 @@ source = mycli addopts = --capture=sys --showlocals --doctest-modules - --cov-report= - --cov=mycli --ignore=setup.py --ignore=mycli/magic.py --ignore=mycli/packages/parseutils.py From ea92f9e42e7a62340db21b854553281c2fc4403c Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sun, 7 May 2017 00:08:22 -0500 Subject: [PATCH 066/627] Fix wrappager path. --- test/features/environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/features/environment.py b/test/features/environment.py index 42ed8df1..61a579e5 100644 --- a/test/features/environment.py +++ b/test/features/environment.py @@ -52,7 +52,7 @@ def before_all(context): } os.environ['PAGER'] = "{0} {1} {2}".format( sys.executable, - os.path.join(PACKAGE_ROOT, '/test/features/wrappager.py'), + os.path.join(PACKAGE_ROOT, 'test/features/wrappager.py'), context.conf['pager_boundary']) context.cn = dbutils.create_db(context.conf['host'], context.conf['user'], From adf6b939196d63cd3db44167ddc67cabfcc5753d Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sun, 7 May 2017 06:44:10 -0500 Subject: [PATCH 067/627] Drop pytest-cov. --- .travis.yml | 4 ++-- requirements-dev.txt | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 49ec67d9..10cb01a8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,13 +7,13 @@ python: - "3.6" install: - - pip install PyMySQL pytest mock codecov pexpect behave pytest-cov + - pip install PyMySQL pytest mock codecov pexpect behave - pip install git+https://github.com/hayd/pep8radius.git - pip install -e . script: - set -e - - py.test --cov-report= --cov=mycli + - coverage run -m py.test - behave test/features # check for pep8 errors, only looking at branch vs master. If there are errors, show diff and return an error code. - pep8radius master --docformatter --error-status || ( pep8radius master --docformatter --diff; false ) diff --git a/requirements-dev.txt b/requirements-dev.txt index c7e7c2c4..cf552f48 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,5 @@ mock pytest -pytest-cov==2.4.0 tox twine==1.8.1 behave From 47dcafb642adfa9a3853629a1fe0293fea451c07 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sun, 7 May 2017 06:44:19 -0500 Subject: [PATCH 068/627] Use absolute path for package root. --- mycli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index 3eb513c8..ca03900f 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -56,7 +56,7 @@ # Query tuples are used for maintaining history Query = namedtuple('Query', ['query', 'successful', 'mutating']) -PACKAGE_ROOT = os.path.dirname(__file__) +PACKAGE_ROOT = os.path.abspath(os.path.dirname(__file__)) # no-op logging handler class NullHandler(logging.Handler): From d4d1f0d88e8a1bd0325bdbbdab658d938371df47 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Mon, 8 May 2017 08:16:22 -0500 Subject: [PATCH 069/627] Move coverage config back to coveragerc. --- .coveragerc | 3 +++ setup.cfg | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..8d3149f6 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[run] +parallel = True +source = mycli diff --git a/setup.cfg b/setup.cfg index 041a857b..5d578a99 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,10 +1,6 @@ [bdist_wheel] universal = 1 -[coverage:run] -parallel = True -source = mycli - [tool:pytest] addopts = --capture=sys --showlocals From b471f5005c5b91963852fdbfca7d4c49697dd397 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Mon, 8 May 2017 08:16:50 -0500 Subject: [PATCH 070/627] Make behave support running from project/test dir. --- test/features/environment.py | 12 +++++++----- test/features/steps/wrappers.py | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/test/features/environment.py b/test/features/environment.py index 61a579e5..f43ff842 100644 --- a/test/features/environment.py +++ b/test/features/environment.py @@ -10,16 +10,18 @@ from steps.wrappers import run_cli, wait_prompt -PACKAGE_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) - def before_all(context): """Set env parameters.""" os.environ['LINES'] = "100" os.environ['COLUMNS'] = "100" os.environ['EDITOR'] = 'ex' - os.environ["COVERAGE_PROCESS_START"] = os.path.join(PACKAGE_ROOT, - 'setup.cfg') + + context.package_root = os.path.abspath( + os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) + + os.environ["COVERAGE_PROCESS_START"] = os.path.join(context.package_root, + '.coveragerc') context.exit_sent = False @@ -52,7 +54,7 @@ def before_all(context): } os.environ['PAGER'] = "{0} {1} {2}".format( sys.executable, - os.path.join(PACKAGE_ROOT, 'test/features/wrappager.py'), + os.path.join(context.package_root, 'test/features/wrappager.py'), context.conf['pager_boundary']) context.cn = dbutils.create_db(context.conf['host'], context.conf['user'], diff --git a/test/features/steps/wrappers.py b/test/features/steps/wrappers.py index 3855ede0..5070f053 100644 --- a/test/features/steps/wrappers.py +++ b/test/features/steps/wrappers.py @@ -38,7 +38,7 @@ def run_cli(context): cmd_parts = [cli_cmd] + run_args cmd = ' '.join(cmd_parts) - context.cli = pexpect.spawnu(cmd, cwd='..') + context.cli = pexpect.spawnu(cmd, cwd=context.package_root) context.exit_sent = False context.currentdb = context.conf['dbname'] From 21cd18b556fcb7b6ea891c2f68ae29d128713bdf Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Mon, 8 May 2017 08:34:11 -0500 Subject: [PATCH 071/627] Make tee files relative to package root. --- test/features/steps/iocommands.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/features/steps/iocommands.py b/test/features/steps/iocommands.py index 5de640ff..6fe36f4a 100644 --- a/test/features/steps/iocommands.py +++ b/test/features/steps/iocommands.py @@ -10,8 +10,8 @@ @when('we start external editor providing a file name') def step_edit_file(context): """Edit file with external editor.""" - context.editor_file_name = '../test_file_{0}.sql'.format( - context.conf['vi']) + context.editor_file_name = os.path.join( + context.package_root, 'test_file_{0}.sql'.format(context.conf['vi'])) if os.path.exists(context.editor_file_name): os.remove(context.editor_file_name) context.cli.sendline('\e {0}'.format( @@ -48,7 +48,8 @@ def step_edit_done_sql(context): @when(u'we tee output') def step_tee_ouptut(context): - context.tee_file_name = '../tee_file_{0}.sql'.format(context.conf['vi']) + context.tee_file_name = os.path.join( + context.package_root, 'tee_file_{0}.sql'.format(context.conf['vi'])) if os.path.exists(context.tee_file_name): os.remove(context.tee_file_name) context.cli.sendline('tee {0}'.format( From ba063c11e38c616ad047f4c02a0cc62dcaf9690d Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Mon, 8 May 2017 09:59:02 -0500 Subject: [PATCH 072/627] Fix test files and config in MANIFEST.in. --- MANIFEST.in | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index d9bc0186..798f07a8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,6 @@ include LICENSE.txt *.md *.rst requirements-dev.txt screenshots/* -include conftest.py .coveragerc pytest.ini test tox.ini +include .coveragerc tox.ini +recursive-include test *.cnf +recursive-include test *.feature +recursive-include test *.py +recursive-include test *.txt From c606deb3e8e86e73294d92bf714298370d6b1e05 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Mon, 8 May 2017 17:03:24 -0500 Subject: [PATCH 073/627] Revert "Drop pytest-cov." This reverts commit adf6b939196d63cd3db44167ddc67cabfcc5753d. --- .travis.yml | 4 ++-- requirements-dev.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 10cb01a8..49ec67d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,13 +7,13 @@ python: - "3.6" install: - - pip install PyMySQL pytest mock codecov pexpect behave + - pip install PyMySQL pytest mock codecov pexpect behave pytest-cov - pip install git+https://github.com/hayd/pep8radius.git - pip install -e . script: - set -e - - coverage run -m py.test + - py.test --cov-report= --cov=mycli - behave test/features # check for pep8 errors, only looking at branch vs master. If there are errors, show diff and return an error code. - pep8radius master --docformatter --error-status || ( pep8radius master --docformatter --diff; false ) diff --git a/requirements-dev.txt b/requirements-dev.txt index cf552f48..c7e7c2c4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,6 @@ mock pytest +pytest-cov==2.4.0 tox twine==1.8.1 behave From 229a38c381f52ff7fc13c33b0ae1952791ed9b5e Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Tue, 9 May 2017 00:49:35 -0500 Subject: [PATCH 074/627] Move Makefile tasks to setup.py. --- .travis.yml | 3 +- MANIFEST.in | 4 +- Makefile | 24 ------------ setup.py | 14 ++++--- tasks.py | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++ tox.ini | 2 +- 6 files changed, 117 insertions(+), 34 deletions(-) delete mode 100644 Makefile mode change 100644 => 100755 setup.py create mode 100644 tasks.py diff --git a/.travis.yml b/.travis.yml index de590fc0..fff93d10 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,7 @@ script: - cd test - behave - cd .. - # check for pep8 errors, only looking at branch vs master. If there are errors, show diff and return an error code. - - pep8radius master --docformatter --error-status || ( pep8radius master --docformatter --diff; false ) + - ./setup.py lint - set +e after_success: diff --git a/MANIFEST.in b/MANIFEST.in index f787b945..af8dd71a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ -include LICENSE.txt *.md *.rst TODO requirements-dev.txt Makefile screenshots/* -include conftest.py .coveragerc pytest.ini test tox.ini +include LICENSE.txt *.md *.rst TODO requirements-dev.txt screenshots/* +include tasks.py conftest.py .coveragerc pytest.ini test tox.ini diff --git a/Makefile b/Makefile deleted file mode 100644 index ad6cfbbc..00000000 --- a/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -.PHONY: clean lint lint-fix test test-all - -help: - @echo "clean - remove all build artifacts" - @echo "lint - check code changes against PEP 8" - @echo "lint-fix - automatically fix PEP 8 violations" - @echo "test - run tests quickly with the current Python" - @echo "test-all - run tests in all environments" - -clean: - rm -rf build dist egg *.egg-info - find . -name '*.py[co]' -exec rm -f {} + - -lint: - pep8radius master --docformatter --error-status || ( pep8radius master --docformatter --diff; false ) - -lint-fix: - pep8radius master --docformatter --in-place - -test: - if pytest ; then cd test && behave ; fi - -test-all: - tox diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 5140b7e9..bc39b29d --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ +#!/usr/bin/env python + import re import ast -import platform from setuptools import setup, find_packages _version_re = re.compile(r'__version__\s+=\s+(.*)') @@ -33,10 +34,13 @@ description=description, long_description=description, install_requires=install_requirements, - entry_points=''' - [console_scripts] - mycli=mycli.main:cli - ''', + entry_points={ + 'console_scripts': ['mycli = mycli.main:cli'], + 'distutils.commands': [ + 'lint = tasks:lint', + 'test = tasks:test', + ], + }, classifiers=[ 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', diff --git a/tasks.py b/tasks.py new file mode 100644 index 00000000..b65c019e --- /dev/null +++ b/tasks.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +"""Common development tasks for setup.py to use.""" + +import re +import subprocess +import sys + +from setuptools import Command + + +class BaseCommand(Command, object): + """The base command for project tasks.""" + + user_options = [] + + default_cmd_options = ('verbose', 'quiet', 'dry_run') + + def __init__(self, *args, **kwargs): + super(BaseCommand, self).__init__(*args, **kwargs) + self.verbose = False + + def initialize_options(self): + """Override the distutils abstract method.""" + pass + + def finalize_options(self): + """Override the distutils abstract method.""" + # Distutils uses incrementing integers for verbosity. + self.verbose = bool(self.verbose) + + def call_and_exit(self, cmd, shell=True): + """Run the *cmd* and exit with the proper exit code.""" + sys.exit(subprocess.call(cmd, shell=shell)) + + def call_in_sequence(self, cmds, shell=True): + """Run multiple commmands in a row, exiting if one fails.""" + for cmd in cmds: + if subprocess.call(cmd, shell=shell) == 1: + sys.exit(1) + + def apply_options(self, cmd, options=()): + """Apply command-line options.""" + for option in (self.default_cmd_options + options): + cmd = self.apply_option(cmd, option, + active=getattr(self, option, False)) + return cmd + + def apply_option(self, cmd, option, active=True): + """Apply a command-line option.""" + return re.sub(r'{{{}\:(?P