diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6cd8675a..21b843b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,3 @@ jobs: TERM: xterm run: | uv run tox -e py${{ matrix.python-version }} - - - name: Run Style Checks - run: uv run tox -e style diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..a765d149 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,30 @@ +name: lint + +on: + pull_request: + paths-ignore: + - '**.md' + - 'AUTHORS' + +jobs: + linters: + name: Linters + runs-on: ubuntu-latest + + steps: + - name: Check out Git repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + # todo + # remember to sync the ruff-check version number with pyproject.toml + # - name: Run ruff check + # uses: astral-sh/ruff-action@9828f49eb4cadf267b40eaa330295c412c68c1f9 # v3.2.2 + # with: + # version: 0.11.5 + + # remember to sync the ruff-check version number with pyproject.toml + - name: Run ruff format + uses: astral-sh/ruff-action@9828f49eb4cadf267b40eaa330295c412c68c1f9 # v3.2.2 + with: + version: 0.11.5 + args: 'format --check' diff --git a/mycli/config.py b/mycli/config.py index 4ce5eff7..a948a4bb 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -51,11 +51,11 @@ def read_config_file(f, list_values=True): try: config = ConfigObj(f, interpolation=False, encoding="utf8", list_values=list_values) except ConfigObjError as e: - log(logger, logging.WARNING, "Unable to parse line {0} of config file " "'{1}'.".format(e.line_number, f)) + log(logger, logging.WARNING, "Unable to parse line {0} of config file '{1}'.".format(e.line_number, f)) log(logger, logging.WARNING, "Using successfully parsed config values.") return e.config except (IOError, OSError) as e: - log(logger, logging.WARNING, "You don't have permission to read " "config file '{0}'.".format(e.filename)) + log(logger, logging.WARNING, "You don't have permission to read config file '{0}'.".format(e.filename)) return None return config diff --git a/mycli/main.py b/mycli/main.py index 1755be90..331feaa6 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -83,12 +83,15 @@ # Query tuples are used for maintaining history Query = namedtuple("Query", ["query", "successful", "mutating"]) -SUPPORT_INFO = "Home: http://mycli.net\n" "Bug tracker: https://github.com/dbcli/mycli/issues" +SUPPORT_INFO = "Home: http://mycli.net\nBug tracker: https://github.com/dbcli/mycli/issues" + class PasswordFileError(Exception): """Base exception for errors related to reading password files.""" + pass + class MyCli(object): default_prompt = "\\t \\u@\\h:\\d> " default_prompt_splitln = "\\u@\\h\\n(\\t):\\d>" @@ -256,7 +259,7 @@ def change_db(self, arg, **_): arg = re.sub(r"``", r"`", arg) self.sqlexecute.change_db(arg) - yield (None, None, None, 'You are now connected to database "%s" as ' 'user "%s"' % (self.sqlexecute.dbname, self.sqlexecute.user)) + yield (None, None, None, 'You are now connected to database "%s" as user "%s"' % (self.sqlexecute.dbname, self.sqlexecute.user)) def execute_from_file(self, arg, **_): if not arg: @@ -308,7 +311,7 @@ def initialize_logging(self): self.echo('Error: Unable to open the log file "{}".'.format(log_file), err=True, fg="red") return - formatter = logging.Formatter("%(asctime)s (%(process)d/%(threadName)s) " "%(name)s %(levelname)s - %(message)s") + formatter = logging.Formatter("%(asctime)s (%(process)d/%(threadName)s) %(name)s %(levelname)s - %(message)s") handler.setFormatter(formatter) @@ -643,7 +646,7 @@ def run_cli(self): else: history = None self.echo( - 'Error: Unable to open the history file "{}". ' "Your query history will not be saved.".format(history_file), + 'Error: Unable to open the history file "{}". Your query history will not be saved.'.format(history_file), err=True, fg="red", ) @@ -1113,7 +1116,7 @@ def get_last_query(self): @click.command() @click.option("-h", "--host", envvar="MYSQL_HOST", help="Host address of the database.") -@click.option("-P", "--port", envvar="MYSQL_TCP_PORT", type=int, help="Port number to use for connection. Honors " "$MYSQL_TCP_PORT.") +@click.option("-P", "--port", envvar="MYSQL_TCP_PORT", type=int, help="Port number to use for connection. Honors $MYSQL_TCP_PORT.") @click.option("-u", "--user", help="User name to connect to the database.") @click.option("-S", "--socket", envvar="MYSQL_UNIX_PORT", help="The socket file to use for connection.") @click.option("-p", "--password", "password", envvar="MYSQL_PWD", type=str, help="Password to connect to the database.") @@ -1139,7 +1142,7 @@ def get_last_query(self): @click.option( "--ssl-verify-server-cert", is_flag=True, - help=('Verify server\'s "Common Name" in its cert against ' "hostname used when connecting. This option is disabled " "by default."), + help=('Verify server\'s "Common Name" in its cert against hostname used when connecting. This option is disabled by default.'), ) # as of 2016-02-15 revocation list is not supported by underling PyMySQL # library (--ssl-crl and --ssl-crlpath options in vanilla mysql client) @@ -1237,7 +1240,7 @@ def cli( try: alias_dsn = mycli.config["alias_dsn"] except KeyError: - click.secho("Invalid DSNs found in the config file. " 'Please check the "[alias_dsn]" section in myclirc.', err=True, fg="red") + click.secho("Invalid DSNs found in the config file. Please check the \"[alias_dsn]\" section in myclirc.", err=True, fg="red") sys.exit(1) except Exception as e: click.secho(str(e), err=True, fg="red") @@ -1293,7 +1296,7 @@ def cli( dsn_uri = mycli.config["alias_dsn"][dsn] except KeyError: click.secho( - "Could not find the specified DSN in the config file. " 'Please check the "[alias_dsn]" section in your ' "myclirc.", + "Could not find the specified DSN in the config file. Please check the \"[alias_dsn]\" section in your myclirc.", err=True, fg="red", ) @@ -1370,7 +1373,7 @@ def cli( if combined_init_cmd: click.echo("Executing init-command: %s" % combined_init_cmd, err=True) - mycli.logger.debug("Launch Params: \n" "\tdatabase: %r" "\tuser: %r" "\thost: %r" "\tport: %r", database, user, host, port) + mycli.logger.debug("Launch Params: \n\tdatabase: %r\tuser: %r\thost: %r\tport: %r", database, user, host, port) # --execute argument if execute: diff --git a/mycli/packages/prompt_utils.py b/mycli/packages/prompt_utils.py index 2cbca5ed..849a008d 100644 --- a/mycli/packages/prompt_utils.py +++ b/mycli/packages/prompt_utils.py @@ -32,7 +32,7 @@ def confirm_destructive_query(queries): * False if the query is destructive and the user doesn't want to proceed. """ - prompt_text = "You're about to run a destructive command.\n" "Do you want to proceed? (y/n)" + prompt_text = "You're about to run a destructive command.\nDo you want to proceed? (y/n)" if is_destructive(queries) and sys.stdin.isatty(): return prompt(prompt_text, type=BOOLEAN_TYPE) diff --git a/mycli/packages/special/dbcommands.py b/mycli/packages/special/dbcommands.py index 4432a22e..549b9c47 100644 --- a/mycli/packages/special/dbcommands.py +++ b/mycli/packages/special/dbcommands.py @@ -116,7 +116,7 @@ def status(cur, **_): output.append(("Connection:", host_info)) - query = "SELECT @@character_set_server, @@character_set_database, " "@@character_set_client, @@character_set_connection LIMIT 1;" + query = "SELECT @@character_set_server, @@character_set_database, @@character_set_client, @@character_set_connection LIMIT 1;" log.debug(query) cur.execute(query) charset = cur.fetchone() diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index e3950c34..8ff0e890 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -98,15 +98,18 @@ def set_expanded_output(val): def is_expanded_output(): return use_expanded_output + @export def set_forced_horizontal_output(val): global force_horizontal_output force_horizontal_output = val + @export def forced_horizontal(): return force_horizontal_output + _logger = logging.getLogger(__name__) diff --git a/pyproject.toml b/pyproject.toml index a8af0b15..a4e1abde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,3 +57,29 @@ include = ["mycli*"] [tool.ruff] line-length = 140 + +[tool.ruff.lint] +select = [ + 'A', + 'I', + 'E', + 'W', + 'F', + 'C4', + 'PIE', + 'TID', +] +ignore = [ + 'E401', # Multiple imports on one line + 'E402', # Module level import not at top of file + 'E501', # Line too long + 'F541', # f-string without placeholders + 'PIE808', # range() starting with 0 +] + +[tool.ruff.format] +quote-style = 'preserve' +exclude = [ + 'build', + 'mycli_dev', +] diff --git a/test/features/environment.py b/test/features/environment.py index a3d3764b..660a9810 100644 --- a/test/features/environment.py +++ b/test/features/environment.py @@ -65,7 +65,7 @@ def before_all(context): _, my_cnf = mkstemp() with open(my_cnf, "w") as f: f.write( - "[client]\n" "pager={0} {1} {2}\n".format( + "[client]\npager={0} {1} {2}\n".format( sys.executable, os.path.join(context.package_root, "test/features/wrappager.py"), context.conf["pager_boundary"] ) ) diff --git a/test/features/steps/auto_vertical.py b/test/features/steps/auto_vertical.py index ad200670..62ebf838 100644 --- a/test/features/steps/auto_vertical.py +++ b/test/features/steps/auto_vertical.py @@ -41,7 +41,7 @@ def step_see_small_results(context): @then("we see large results in vertical format") def step_see_large_results(context): rows = ["{n:3}| {n}".format(n=str(n)) for n in range(1, 50)] - expected = "***************************[ 1. row ]" "***************************\r\n" + "{}\r\n".format("\r\n".join(rows) + "\r\n") + expected = "***************************[ 1. row ]***************************\r\n" + "{}\r\n".format("\r\n".join(rows) + "\r\n") wrappers.expect_pager(context, expected, timeout=10) wrappers.expect_exact(context, "1 row in set", timeout=2) diff --git a/test/features/steps/connection.py b/test/features/steps/connection.py index 80d0653a..cde7d48c 100644 --- a/test/features/steps/connection.py +++ b/test/features/steps/connection.py @@ -32,7 +32,7 @@ def status_contains(context, expression): @when("we create my.cnf file") def step_create_my_cnf_file(context): - my_cnf = "[client]\n" f"host = {HOST}\n" f"port = {PORT}\n" f"user = {USER}\n" f"password = {PASSWORD}\n" + my_cnf = f"[client]\nhost = {HOST}\nport = {PORT}\nuser = {USER}\npassword = {PASSWORD}\n" with open(MY_CNF_PATH, "w") as f: f.write(my_cnf) @@ -40,7 +40,7 @@ def step_create_my_cnf_file(context): @when("we create mylogin.cnf file") def step_create_mylogin_cnf_file(context): os.environ.pop("MYSQL_TEST_LOGIN_FILE", None) - mylogin_cnf = f"[{TEST_LOGIN_PATH}]\n" f"host = {HOST}\n" f"port = {PORT}\n" f"user = {USER}\n" f"password = {PASSWORD}\n" + mylogin_cnf = f"[{TEST_LOGIN_PATH}]\nhost = {HOST}\nport = {PORT}\nuser = {USER}\npassword = {PASSWORD}\n" with open(MYLOGIN_CNF_PATH, "wb") as f: input_file = io.StringIO(mylogin_cnf) f.write(encrypt_mylogin_cnf(input_file).read()) diff --git a/test/features/steps/wrappers.py b/test/features/steps/wrappers.py index f9325c6e..6e1115fe 100644 --- a/test/features/steps/wrappers.py +++ b/test/features/steps/wrappers.py @@ -81,9 +81,7 @@ def add_arg(name, key, value): try: cli_cmd = context.conf["cli_command"] except KeyError: - cli_cmd = ('{0!s} -c "' "import coverage ; " "coverage.process_startup(); " "import mycli.main; " "mycli.main.cli()" '"').format( - sys.executable - ) + cli_cmd = ('{0!s} -c "import coverage ; coverage.process_startup(); import mycli.main; mycli.main.cli()"').format(sys.executable) cmd_parts = [cli_cmd] + rendered_args cmd = " ".join(cmd_parts) diff --git a/test/test_main.py b/test/test_main.py index 3a757bcc..147ab324 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -93,7 +93,7 @@ def test_batch_mode(executor): run(executor, """create table test(a text)""") run(executor, """insert into test values('abc'), ('def'), ('ghi')""") - sql = "select count(*) from test;\n" "select * from test limit 1;" + sql = "select count(*) from test;\nselect * from test limit 1;" runner = CliRunner() result = runner.invoke(cli, args=CLI_ARGS, input=sql) @@ -107,7 +107,7 @@ def test_batch_mode_table(executor): run(executor, """create table test(a text)""") run(executor, """insert into test values('abc'), ('def'), ('ghi')""") - sql = "select count(*) from test;\n" "select * from test limit 1;" + sql = "select count(*) from test;\nselect * from test limit 1;" runner = CliRunner() result = runner.invoke(cli, args=CLI_ARGS + ["-t"], input=sql) @@ -543,7 +543,7 @@ def test_init_command_arg(executor): @dbtest def test_init_command_multiple_arg(executor): init_command = "set sql_select_limit=2000; set max_join_size=20000" - sql = 'show variables like "sql_select_limit";\n' 'show variables like "max_join_size"' + sql = 'show variables like "sql_select_limit";\nshow variables like "max_join_size"' runner = CliRunner() result = runner.invoke(cli, args=CLI_ARGS + ["--init-command", init_command], input=sql) @@ -554,6 +554,7 @@ def test_init_command_multiple_arg(executor): assert expected_sql_select_limit in result.output assert expected_max_join_size in result.output + @dbtest def test_global_init_commands(executor): """Tests that global init-commands from config are executed by default.""" diff --git a/test/test_parseutils.py b/test/test_parseutils.py index 09252993..189c31bf 100644 --- a/test/test_parseutils.py +++ b/test/test_parseutils.py @@ -122,24 +122,24 @@ def test_query_starts_with_comment(): def test_queries_start_with(): - sql = "# comment\n" "show databases;" "use foo;" + sql = "# comment\nshow databases;use foo;" assert queries_start_with(sql, ("show", "select")) is True assert queries_start_with(sql, ("use", "drop")) is True assert queries_start_with(sql, ("delete", "update")) is False def test_is_destructive(): - sql = "use test;\n" "show databases;\n" "drop database foo;" + sql = "use test;\nshow databases;\ndrop database foo;" assert is_destructive(sql) is True def test_is_destructive_update_with_where_clause(): - sql = "use test;\n" "show databases;\n" "UPDATE test SET x = 1 WHERE id = 1;" + sql = "use test;\nshow databases;\nUPDATE test SET x = 1 WHERE id = 1;" assert is_destructive(sql) is False def test_is_destructive_update_without_where_clause(): - sql = "use test;\n" "show databases;\n" "UPDATE test SET x = 1;" + sql = "use test;\nshow databases;\nUPDATE test SET x = 1;" assert is_destructive(sql) is True @@ -167,7 +167,7 @@ def test_query_has_where_clause(sql, has_where_clause): ("drop database foo; create database bar", "foo", True), ("select bar from foo; drop database bazz", "foo", False), ("select bar from foo; drop database bazz", "bazz", True), - ("-- dropping database \n " "drop -- really dropping \n " "schema abc -- now it is dropped", "abc", True), + ("-- dropping database \n drop -- really dropping \n schema abc -- now it is dropped", "abc", True), ], ) def test_is_dropping_database(sql, dbname, is_dropping): diff --git a/test/test_sqlexecute.py b/test/test_sqlexecute.py index f71deea0..88be7ffc 100644 --- a/test/test_sqlexecute.py +++ b/test/test_sqlexecute.py @@ -44,7 +44,7 @@ def test_bools(executor): @dbtest def test_binary(executor): run(executor, """create table bt(geom linestring NOT NULL)""") - run(executor, "INSERT INTO bt VALUES " "(ST_GeomFromText('LINESTRING(116.37604 39.73979,116.375 39.73965)'));") + run(executor, "INSERT INTO bt VALUES (ST_GeomFromText('LINESTRING(116.37604 39.73979,116.375 39.73965)'));") results = run(executor, """select * from bt""") geom = ( @@ -139,7 +139,7 @@ def test_favorite_query_multiple_statement(executor): run(executor, "insert into test values('abc')") run(executor, "insert into test values('def')") - results = run(executor, "\\fs test-ad select * from test where a like 'a%'; " "select * from test where a like 'd%'") + results = run(executor, "\\fs test-ad select * from test where a like 'a%'; select * from test where a like 'd%'") assert_result_equal(results, status="Saved.") results = run(executor, "\\f test-ad")