From 853c1070f99d6a5caf524daa6960c5167018e522 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 9 Mar 2025 08:29:34 +0100 Subject: [PATCH] Merging upstream version 4.2.0. Signed-off-by: Daniel Baumann --- .github/workflows/ci.yml | 13 ++--- .gitignore | 1 + AUTHORS | 4 ++ README.rst | 9 ++-- changelog.rst | 18 +++++++ pgcli/__init__.py | 2 +- pgcli/main.py | 40 ++++++++++---- pgcli/pgcompleter.py | 23 +++++--- pyproject.toml | 72 ++++++++++++++++++++++++++ setup.py | 71 ------------------------- tests/features/basic_commands.feature | 4 ++ tests/features/steps/basic_commands.py | 13 +++++ tests/test_pgcompleter.py | 24 +++++++++ tox.ini | 2 +- 14 files changed, 197 insertions(+), 99 deletions(-) delete mode 100644 setup.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 007178f..227047d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] services: postgres: @@ -77,12 +77,13 @@ jobs: - name: Run unit tests run: coverage run --source pgcli -m pytest - - name: Run integration tests - env: - PGUSER: postgres - PGPASSWORD: postgres + # - name: Run integration tests + # env: + # PGUSER: postgres + # PGPASSWORD: postgres + # TERM: xterm - run: behave tests/features --no-capture + # run: behave tests/features --no-capture - name: Check changelog for ReST compliance run: docutils --halt=warning changelog.rst >/dev/null diff --git a/.gitignore b/.gitignore index b993cb9..7a33867 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ htmlcov/ nosetests.xml coverage.xml .pytest_cache +tests/behave.ini # Translations *.mo diff --git a/AUTHORS b/AUTHORS index 9f33ff5..4bbaba2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -134,6 +134,10 @@ Contributors: * Antonio Aguilar (crazybolillo) * Andrew M. MacFie (amacfie) * saucoide + * Chris Rose (offbyone/offby1) + * Mathieu Dupuy (deronnax) + * Chris Novakovic + * Josh Lynch (josh-lynch) Creator: -------- diff --git a/README.rst b/README.rst index 56695cc..231de2c 100644 --- a/README.rst +++ b/README.rst @@ -179,7 +179,7 @@ Alternatively, you can install ``pgcli`` as a python package using a package manager called called ``pip``. You will need postgres installed on your system for this to work. -In depth getting started guide for ``pip`` - https://pip.pypa.io/en/latest/installing.html. +In depth getting started guide for ``pip`` - https://pip.pypa.io/en/latest/installation/ :: @@ -209,7 +209,7 @@ If pip is not installed check if easy_install is available on the system. Linux: ====== -In depth getting started guide for ``pip`` - https://pip.pypa.io/en/latest/installing.html. +In depth getting started guide for ``pip`` - https://pip.pypa.io/en/latest/installation/ Check if pip is already available in your system. @@ -352,7 +352,10 @@ choice: In [3]: my_result = _ -Pgcli dropped support for Python<3.8 as of 4.0.0. If you need it, install ``pgcli <= 4.0.0``. +Pgcli dropped support for: + +* Python<3.8 as of 4.0.0. +* Python<3.9 as of 4.2.0. Thanks: ------- diff --git a/changelog.rst b/changelog.rst index 744e903..0b219ea 100644 --- a/changelog.rst +++ b/changelog.rst @@ -1,3 +1,21 @@ +4.2.0 (2025-03-06) +================== + +Features +-------- +* Add a `--ping` command line option; allows pgcli to replace `pg_isready` +* Changed the packaging metadata from setup.py to pyproject.toml + +Bug fixes: +---------- +* Avoid raising `NameError` when exiting unsuccessfully in some cases +* Use configured `alias_map_file` to generate table aliases if available. + +Internal: +--------- + +* Drop support for Python 3.8 and add 3.13. + 4.1.0 (2024-03-09) ================== diff --git a/pgcli/__init__.py b/pgcli/__init__.py index 7039708..0fd7811 100644 --- a/pgcli/__init__.py +++ b/pgcli/__init__.py @@ -1 +1 @@ -__version__ = "4.1.0" +__version__ = "4.2.0" diff --git a/pgcli/main.py b/pgcli/main.py index 056a940..d4c6dbf 100644 --- a/pgcli/main.py +++ b/pgcli/main.py @@ -610,7 +610,7 @@ class PGCli: click.secho( f"service '{service}' was not found in {file}", err=True, fg="red" ) - exit(1) + sys.exit(1) self.connect( database=service_config.get("dbname"), host=service_config.get("host"), @@ -710,7 +710,7 @@ class PGCli: self.logger.handlers = logger_handlers self.logger.error("traceback: %r", traceback.format_exc()) click.secho(str(e), err=True, fg="red") - exit(1) + sys.exit(1) self.logger.handlers = logger_handlers atexit.register(self.ssh_tunnel.stop) @@ -763,7 +763,7 @@ class PGCli: self.logger.debug("Database connection failed: %r.", e) self.logger.error("traceback: %r", traceback.format_exc()) click.secho(str(e), err=True, fg="red") - exit(1) + sys.exit(1) self.pgexecute = pgexecute @@ -1463,6 +1463,13 @@ class PGCli: is_flag=True, help="list available databases, then exit.", ) +@click.option( + "--ping", + "ping_database", + is_flag=True, + default=False, + help="Check database connectivity, then exit.", +) @click.option( "--auto-vertical-output", is_flag=True, @@ -1504,6 +1511,7 @@ def cli( prompt, prompt_dsn, list_databases, + ping_database, auto_vertical_output, list_dsn, warn, @@ -1543,7 +1551,7 @@ def cli( err=True, fg="red", ) - exit(1) + sys.exit(1) if ssh_tunnel and not SSH_TUNNEL_SUPPORT: click.secho( @@ -1552,7 +1560,7 @@ def cli( err=True, fg="red", ) - exit(1) + sys.exit(1) pgcli = PGCli( prompt_passwd, @@ -1581,8 +1589,8 @@ def cli( service = database[8:] elif os.getenv("PGSERVICE") is not None: service = os.getenv("PGSERVICE") - # because option --list or -l are not supposed to have a db name - if list_databases: + # because option --ping, --list or -l are not supposed to have a db name + if list_databases or ping_database: database = "postgres" if dsn != "": @@ -1596,7 +1604,7 @@ def cli( err=True, fg="red", ) - exit(1) + sys.exit(1) except Exception: click.secho( "Invalid DSNs found in the config file. " @@ -1604,7 +1612,7 @@ def cli( err=True, fg="red", ) - exit(1) + sys.exit(1) pgcli.connect_uri(dsn_config) pgcli.dsn_alias = dsn elif "://" in database: @@ -1626,6 +1634,20 @@ def cli( sys.exit(0) + if ping_database: + try: + list(pgcli.pgexecute.run("SELECT 1")) + except Exception: + click.secho( + "Could not connect to the database. Please check that the database is running.", + err=True, + fg="red", + ) + sys.exit(1) + else: + click.echo("PONG") + sys.exit(0) + pgcli.logger.debug( "Launch Params: \n" "\tdatabase: %r" "\tuser: %r" "\thost: %r" "\tport: %r", database, diff --git a/pgcli/pgcompleter.py b/pgcli/pgcompleter.py index 17fc540..8df2958 100644 --- a/pgcli/pgcompleter.py +++ b/pgcli/pgcompleter.py @@ -63,10 +63,17 @@ normalize_ref = lambda ref: ref if ref[0] == '"' else '"' + ref.lower() + '"' def generate_alias(tbl, alias_map=None): - """Generate a table alias, consisting of all upper-case letters in - the table name, or, if there are no upper-case letters, the first letter + - all letters preceded by _ - param tbl - unescaped name of the table to alias + """Generate a table alias. + + Given a table name will return an alias for that table using the first of + the following options there's a match for. + + 1. The predefined alias for table defined in the alias_map. + 2. All upper-case letters in the table name. + 3. The first letter of the table name and all letters preceded by _ + + :param tbl: unescaped name of the table to alias + :param alias_map: optional mapping of predefined table aliases """ if alias_map and tbl in alias_map: return alias_map[tbl] @@ -528,7 +535,7 @@ class PGCompleter(Completer): scoped_cols = self.populate_scoped_cols(tables, suggestion.local_tables) def make_cand(name, ref): - synonyms = (name, generate_alias(self.case(name))) + synonyms = (name, generate_alias(self.case(name), alias_map=self.alias_map)) return Candidate(qualify(name, ref), 0, "column", synonyms) def flat_cols(): @@ -601,7 +608,7 @@ class PGCompleter(Completer): tbl = self.case(tbl) tbls = {normalize_ref(t.ref) for t in tbls} if self.generate_aliases: - tbl = generate_alias(self.unescape_name(tbl)) + tbl = generate_alias(self.unescape_name(tbl), alias_map=self.alias_map) if normalize_ref(tbl) not in tbls: return tbl elif tbl[0] == '"': @@ -644,7 +651,7 @@ class PGCompleter(Completer): join = "{0} ON {0}.{1} = {2}.{3}".format( c(left.tbl), c(left.col), rtbl.ref, c(right.col) ) - alias = generate_alias(self.case(left.tbl)) + alias = generate_alias(self.case(left.tbl), alias_map=self.alias_map) synonyms = [ join, "{0} ON {0}.{1} = {2}.{3}".format( @@ -845,7 +852,7 @@ class PGCompleter(Completer): cased_tbl = self.case(tbl.name) if do_alias: alias = self.alias(cased_tbl, suggestion.table_refs) - synonyms = (cased_tbl, generate_alias(cased_tbl)) + synonyms = (cased_tbl, generate_alias(cased_tbl, alias_map=self.alias_map)) maybe_alias = (" " + alias) if do_alias else "" maybe_schema = (self.case(tbl.schema) + ".") if tbl.schema else "" suffix = self._arg_list_cache[arg_mode][tbl.meta] if arg_mode else "" diff --git a/pyproject.toml b/pyproject.toml index 8477d72..d714282 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,75 @@ +[project] +name = "pgcli" +authors = [{ name = "Pgcli Core Team", email = "pgcli-dev@googlegroups.com" }] +license = { text = "BSD" } +description = "CLI for Postgres Database. With auto-completion and syntax highlighting." +readme = "README.rst" +classifiers = [ + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: Unix", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: SQL", + "Topic :: Database", + "Topic :: Database :: Front-Ends", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries :: Python Modules", +] +urls = { Homepage = "https://pgcli.com" } +requires-python = ">=3.9" +dependencies = [ + "pgspecial>=2.0.0", + "click >= 4.1", + "Pygments>=2.0", # Pygments has to be Capitalcased. WTF? + # We still need to use pt-2 unless pt-3 released on Fedora32 + # see: https://github.com/dbcli/pgcli/pull/1197 + "prompt_toolkit>=2.0.6,<4.0.0", + "psycopg >= 3.0.14; sys_platform != 'win32'", + "psycopg-binary >= 3.0.14; sys_platform == 'win32'", + "sqlparse >=0.3.0,<0.6", + "configobj >= 5.0.6", + "cli_helpers[styles] >= 2.2.1", + # setproctitle is used to mask the password when running `ps` in command line. + # But this is not necessary in Windows since the password is never shown in the + # task manager. Also setproctitle is a hard dependency to install in Windows, + # so we'll only install it if we're not in Windows. + "setproctitle >= 1.1.9; sys_platform != 'win32' and 'CYGWIN' not in sys_platform", +] +dynamic = ["version"] + + +[project.scripts] +pgcli = "pgcli.main:cli" + +[project.optional-dependencies] +keyring = ["keyring >= 12.2.0"] +sshtunnel = ["sshtunnel >= 0.4.0"] + +[build-system] +requires = ["setuptools>=61.2"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +include-package-data = false + +[tool.setuptools.dynamic] +version = { attr = "pgcli.__version__" } + +[tool.setuptools.packages] +find = { namespaces = false } + +[tool.setuptools.package-data] +pgcli = [ + "pgclirc", + "packages/pgliterals/pgliterals.json", +] + [tool.black] line-length = 88 target-version = ['py38'] diff --git a/setup.py b/setup.py deleted file mode 100644 index f4606c2..0000000 --- a/setup.py +++ /dev/null @@ -1,71 +0,0 @@ -import platform -from setuptools import setup, find_packages - -from pgcli import __version__ - -description = "CLI for Postgres Database. With auto-completion and syntax highlighting." - -install_requirements = [ - "pgspecial>=2.0.0", - "click >= 4.1", - "Pygments>=2.0", # Pygments has to be Capitalcased. WTF? - # We still need to use pt-2 unless pt-3 released on Fedora32 - # see: https://github.com/dbcli/pgcli/pull/1197 - "prompt_toolkit>=2.0.6,<4.0.0", - "psycopg >= 3.0.14; sys_platform != 'win32'", - "psycopg-binary >= 3.0.14; sys_platform == 'win32'", - "sqlparse >=0.3.0,<0.6", - "configobj >= 5.0.6", - "cli_helpers[styles] >= 2.2.1", -] - - -# setproctitle is used to mask the password when running `ps` in command line. -# But this is not necessary in Windows since the password is never shown in the -# task manager. Also setproctitle is a hard dependency to install in Windows, -# so we'll only install it if we're not in Windows. -if platform.system() != "Windows" and not platform.system().startswith("CYGWIN"): - install_requirements.append("setproctitle >= 1.1.9") - -setup( - name="pgcli", - author="Pgcli Core Team", - author_email="pgcli-dev@googlegroups.com", - version=__version__, - license="BSD", - url="http://pgcli.com", - packages=find_packages(), - package_data={"pgcli": ["pgclirc", "packages/pgliterals/pgliterals.json"]}, - description=description, - long_description=open("README.rst").read(), - install_requires=install_requirements, - dependency_links=[ - "http://github.com/psycopg/repo/tarball/master#egg=psycopg-3.0.10" - ], - extras_require={ - "keyring": ["keyring >= 12.2.0"], - "sshtunnel": ["sshtunnel >= 0.4.0"], - }, - python_requires=">=3.8", - entry_points=""" - [console_scripts] - pgcli=pgcli.main:cli - """, - classifiers=[ - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Operating System :: Unix", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: SQL", - "Topic :: Database", - "Topic :: Database :: Front-Ends", - "Topic :: Software Development", - "Topic :: Software Development :: Libraries :: Python Modules", - ], -) diff --git a/tests/features/basic_commands.feature b/tests/features/basic_commands.feature index ee497b9..7e975dc 100644 --- a/tests/features/basic_commands.feature +++ b/tests/features/basic_commands.feature @@ -51,6 +51,10 @@ Feature: run the cli, When we list databases then we see list of databases + Scenario: ping databases + When we ping the database + then we get a pong response + Scenario: run the cli with --username When we launch dbcli using --username and we send "\?" command diff --git a/tests/features/steps/basic_commands.py b/tests/features/steps/basic_commands.py index 687bdc0..00ac277 100644 --- a/tests/features/steps/basic_commands.py +++ b/tests/features/steps/basic_commands.py @@ -26,6 +26,19 @@ def step_see_list_databases(context): context.cmd_output = None +@when("we ping the database") +def step_ping_database(context): + cmd = ["pgcli", "--ping"] + context.cmd_output = subprocess.check_output(cmd, cwd=context.package_root) + + +@then("we get a pong response") +def step_get_pong_response(context): + # exit code 0 is implied by the presence of cmd_output here, which + # is only set on a successful run. + assert context.cmd_output.strip() == b"PONG", f"Output was {context.cmd_output}" + + @when("we run dbcli") def step_run_cli(context): wrappers.run_cli(context) diff --git a/tests/test_pgcompleter.py b/tests/test_pgcompleter.py index 909fa0b..4b2d9d0 100644 --- a/tests/test_pgcompleter.py +++ b/tests/test_pgcompleter.py @@ -1,5 +1,7 @@ +import json import pytest from pgcli import pgcompleter +import tempfile def test_load_alias_map_file_missing_file(): @@ -47,12 +49,34 @@ def test_generate_alias_uses_first_char_and_every_preceded_by_underscore( "table_name, alias_map, alias", [ ("some_table", {"some_table": "my_alias"}, "my_alias"), + pytest.param( + "some_other_table", {"some_table": "my_alias"}, "sot", id="no_match_in_map" + ), ], ) def test_generate_alias_can_use_alias_map(table_name, alias_map, alias): assert pgcompleter.generate_alias(table_name, alias_map) == alias +@pytest.mark.parametrize( + "table_name, alias_map, alias", + [ + ("some_table", {"some_table": "my_alias"}, "my_alias"), + ], +) +def test_pgcompleter_alias_uses_configured_alias_map(table_name, alias_map, alias): + with tempfile.NamedTemporaryFile(mode="w", suffix=".json") as alias_map_file: + alias_map_file.write(json.dumps(alias_map)) + alias_map_file.seek(0) + completer = pgcompleter.PGCompleter( + settings={ + "generate_aliases": True, + "alias_map_file": alias_map_file.name, + } + ) + assert completer.alias(table_name, []) == alias + + @pytest.mark.parametrize( "table_name, alias_map, alias", [ diff --git a/tox.ini b/tox.ini index 7d33698..554d66d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py38, py39, py310, py311, py312 +envlist = py39, py310, py311, py312, py313 [testenv] deps = pytest>=2.7.0,<=3.0.7 mock>=1.0.1