Skip to content

Commit

Permalink
Dependencies: Update to psycopg>=3.0 (#38)
Browse files Browse the repository at this point in the history
The `database` key in the DSN was changed to `dbname` in psycopg v3. To
prevent breaking existing code, the key is automatically converted in
the `PGSU` constructor and the `_execute_su_psql` function, and  a
warning is emitted.
  • Loading branch information
sphuber authored May 30, 2024
1 parent 7a05bbc commit 18a01a3
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 32 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
pip freeze
- name: Run test suite
run: pytest --cov-report xml
run: pytest --cov-report xml -sv

- name: Upload coverage report
if: github.repository == 'aiidateam/pgsu'
Expand Down Expand Up @@ -101,7 +101,7 @@ jobs:
- name: Run test suite
env:
PGSU_TEST_PASSWORD: ${{ matrix.postgres-pw}}
run: pytest --cov-report xml
run: pytest --cov-report xml -sv

- name: Upload coverage report
if: github.repository == 'aiidateam/pgsu'
Expand Down Expand Up @@ -151,7 +151,7 @@ jobs:
env:
PGSU_TEST_PASSWORD: ${{ matrix.postgres-pw}}
PGSU_TEST_PORT: ${{ job.services.postgres.ports[5432] }}
run: pytest --cov-report xml
run: pytest --cov-report xml -sv

- name: Upload coverage report
if: github.repository == 'aiidateam/pgsu'
Expand Down Expand Up @@ -237,7 +237,7 @@ jobs:
pip freeze
- name: Run test suite
run: pytest --cov-report xml
run: pytest --cov-report xml -sv

- name: Upload coverage report
if: github.repository == 'aiidateam/pgsu'
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

Connect to an existing PostgreSQL cluster as a PostgreSQL [SUPERUSER](https://www.postgresql.org/docs/current/sql-createrole.html) and execute SQL commands.

[`psycopg2`](https://pypi.org/project/psycopg2/) has a great API for interacting with PostgreSQL, once you provide it with the connection parameters for a given database.
[`psycopg`](https://pypi.org/project/psycopg/) has a great API for interacting with PostgreSQL, once you provide it with the connection parameters for a given database.
However, what if your desired database and database user do not yet exist?
In order to create them, you will need to connect to PostgreSQL as a SUPERUSER.

Expand All @@ -19,7 +19,7 @@ In order to create them, you will need to connect to PostgreSQL as a SUPERUSER.
* [Ubuntu 18.04](https://github.com/actions/virtual-environments/blob/master/images/linux/Ubuntu1804-README.md) & PostgreSQL docker container
* [MacOS 12](https://github.com/actions/virtual-environments/blob/master/images/macos/macos-12-Readme.md) and PostgreSQL installed via `conda`
* [Windows Server 2019](https://github.com/actions/virtual-environments/blob/master/images/win/Windows2019-Readme.md) and PostgreSQL installed via `conda`
* uses [psycopg2](http://initd.org/psycopg/docs/index.html) to connect if possible
* uses [psycopg](http://initd.org/psycopg/docs/index.html) to connect if possible
* can use `sudo` to become the `postgres` UNIX user if necessary/possible (default Ubuntu PostgreSQL setups)

## Usage
Expand Down
55 changes: 34 additions & 21 deletions pgsu/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import os
from enum import IntEnum
import subprocess
import warnings

# By default, try "sudo" only when 'postgres' user exists
DEFAULT_POSTGRES_UNIX_USER = 'postgres'
Expand All @@ -29,7 +30,7 @@
'port': 5432,
'user': DEFAULT_POSTGRES_SUPERUSER,
'password': None,
'database': 'template1',
'dbname': 'template1',
}

LOGGER = logging.getLogger('pgsu')
Expand All @@ -48,7 +49,7 @@ class PGSU:
"""
Connect to an existing PostgreSQL cluster as the `postgres` superuser and execute SQL commands.
Tries to use psycopg2 with a fallback to psql subcommands (using ``sudo su`` to run as postgres user).
Tries to use psycopg with a fallback to psql subcommands (using ``sudo su`` to run as postgres user).
Simple Example::
Expand Down Expand Up @@ -76,13 +77,13 @@ def __init__(self,
:param interactive: use True for verdi commands
:param quiet: use False to show warnings/exceptions
:param dsn: psycopg dictionary containing keys like 'host', 'user', 'port', 'database'.
:param dsn: psycopg dictionary containing keys like 'host', 'user', 'port', 'dbname'.
It is sufficient to provide only those values that deviate from the defaults.
:param determine_setup: Whether to determine setup upon instantiation.
You may set this to False and use the 'determine_setup()' method instead.
:param try_sudo: If connection via psycopg2 fails, whether to try and use `sudo` to become
:param try_sudo: If connection via psycopg fails, whether to try and use `sudo` to become
the `postgres_unix_user` and run commands using passwordless `psql`.
:param postgres_unix_user: UNIX user to try to "become", if connection via psycopg2 fails
:param postgres_unix_user: UNIX user to try to "become", if connection via psycopg fails
"""
self.interactive = interactive
if not quiet:
Expand All @@ -98,6 +99,12 @@ def __init__(self,
if dsn is not None:
self.dsn.update(dsn)

if 'database' in self.dsn:
warnings.warn(
'The dsn contained the key `database` which was renamed to `dbname` in psycopg v3. '
'Renamed the database key to dbname', UserWarning)
self.dsn['dbname'] = self.dsn.pop('database')

self.try_sudo = try_sudo
self.postgres_unix_user = postgres_unix_user

Expand Down Expand Up @@ -126,7 +133,7 @@ def execute(self, command, **kwargs):
def determine_setup(self):
"""Determine how to connect as the postgres superuser.
Depending on how postgres is set up, psycopg2 can be used to create dbs and db users,
Depending on how postgres is set up, psycopg can be used to create dbs and db users,
otherwise a subprocess has to be used that executes psql as an os user with appropriate permissions.
Note: We aim to connect as a superuser (typically 'postgres') with privileges to manipulate (create/drop)
Expand All @@ -137,8 +144,8 @@ def determine_setup(self):
"""
dsn = self.dsn.copy()

# Try to connect as a postgres superuser via psycopg2 (equivalent to using psql).
LOGGER.debug('Trying to connect via "psycopg2"...')
# Try to connect as a postgres superuser via psycopg (equivalent to using psql).
LOGGER.debug('Trying to connect via "psycopg"...')
for pg_user in unique_list([self.dsn.get('user'), None]):
dsn['user'] = pg_user
# First try the host specified (works if 'host' has setting 'trust' in pg_hba.conf).
Expand Down Expand Up @@ -205,16 +212,16 @@ def prompt_for_dsn(dsn):
click.echo('Please provide PostgreSQL connection info:')

# Note: Using '' as the prompt default is necessary to allow users to leave the field empty.
# Using `None` in the dictionary is necessary in order for psycopg2 to interpret the value as not provided.
# Using `None` in the dictionary is necessary in order for to interpret the value as not provided.
dsn_new = {}
dsn_new['host'] = click.prompt(
'postgres host', default=dsn.get('host') or '', type=str) or None
dsn_new['port'] = click.prompt(
'postgres port', default=dsn.get('port'), type=int) or None
dsn_new['user'] = click.prompt(
'postgres super user', default=dsn.get('user'), type=str) or None
dsn_new['database'] = click.prompt(
'database', default=dsn.get('database'), type=str) or None
dsn_new['dbname'] = click.prompt(
'dbname', default=dsn.get('dbname'), type=str) or None
dsn_new['password'] = click.prompt(
'postgres password of {dsn_new["user"]}',
#hide_input=True, # this breaks the input mocking in the tests. could make this configurable instead
Expand All @@ -226,11 +233,11 @@ def prompt_for_dsn(dsn):

def _try_connect_psycopg(**kwargs):
"""
try to start a psycopg2 connection.
try to start a psycopg connection.
:return: True if successful, False otherwise
"""
from psycopg2 import connect # pylint: disable=import-outside-toplevel
from psycopg import connect # pylint: disable=import-outside-toplevel
success = False
try:
conn = connect(**kwargs)
Expand All @@ -244,17 +251,17 @@ def _try_connect_psycopg(**kwargs):

def _execute_psyco(command, dsn):
"""
executes a postgres commandline through psycopg2
executes a postgres commandline through psycopg
:param command: A psql command line as a str
:param dsn: will be forwarded to psycopg2.connect
:param dsn: will be forwarded to psycopg.connect
"""
import psycopg2 # pylint: disable=import-outside-toplevel
import psycopg # pylint: disable=import-outside-toplevel

conn = None
output = None
try:
conn = psycopg2.connect(**dsn)
conn = psycopg.connect(**dsn)
conn.autocommit = True
with conn.cursor() as cursor:
cursor.execute(command)
Expand Down Expand Up @@ -308,14 +315,20 @@ def _execute_su_psql(command, dsn, interactive=False):
Logs any output on 'stderr' to the pgsu logger at 'warning' level.
:param command: A psql command line as a str
:param dsn: connection details to forward to psql, signature as in psycopg2.connect
:param dsn: connection details to forward to psql, signature as in psycopg.connect
:param interactive: If False, `sudo` won't ask for a password and fail if one is required.
"""
psql_option_str = ''

database = dsn.get('database')
if database:
psql_option_str += f'-d {database}'
if 'database' in dsn:
warnings.warn(
'The dsn contained the key `database` which was renamed to `dbname` in psycopg v3. '
'Renamed the database key to dbname', UserWarning)
dsn['dbname'] = dsn.pop('database')

dbname = dsn.get('dbname')
if dbname:
psql_option_str += f'-d {dbname}'

# to do: Forward password to psql; ignore host only when the password is None. # pylint: disable=fixme
# Note: There is currently no known postgresql setup that needs this, though
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ classifiers =
packages = find:
install_requires =
click
psycopg2-binary>=2.8.3
psycopg[binary]>=3.0
python_requires = ~=3.7

[options.packages.find]
Expand Down
8 changes: 4 additions & 4 deletions tests/test_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import sys
from contextlib import contextmanager
from io import StringIO
import psycopg2
import psycopg

import conftest
from pgsu import PGSU, DEFAULT_DSN
Expand All @@ -33,9 +33,9 @@ def test_grant_priv(pgsu, user, database): # pylint: disable=unused-argument
'port': pgsu.dsn.get('port'),
'user': user,
'password': conftest.DEFAULT_PASSWORD,
'database': database,
'dbname': database,
}
conn = psycopg2.connect(**dsn)
conn = psycopg.connect(**dsn) # pylint: disable=missing-kwoa
conn.close()


Expand All @@ -46,7 +46,7 @@ def input_dsn(dsn):
See https://stackoverflow.com/a/36491341/1069467
"""
inputs = []
for key in ['host', 'port', 'user', 'database', 'password']:
for key in ['host', 'port', 'user', 'dbname', 'password']:
inputs.append(str(dsn.get(key, '')))

input_str = str(os.linesep.join(inputs) + os.linesep)
Expand Down

0 comments on commit 18a01a3

Please sign in to comment.