Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Lightweight CI for pull requests (and pushes to main).
#
# The full multi-platform wheel build + test suite lives in
# build-and-test.yml and is too heavy to run on every PR, so this
# workflow gives fast feedback instead:
# - lint: ruff check over the whole repo
# - test: install the package with test extras and run pytest.
# Tests that need a running PostgreSQL server (bundled binaries
# built via `make`, see tests/conftest.py) are skipped here; they
# still run in build-and-test.yml where the binaries exist.
name: CI

on:
pull_request:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
name: Lint (ruff)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install ruff
run: python -m pip install "ruff==0.9.5"
- name: Ruff check
run: ruff check .
- name: Compile check
run: python -m compileall -q src tests

test:
name: Tests (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install package with test extras
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[test]"
- name: Smoke import
run: python -c "import pgembed; print(pgembed.list_extensions())"
- name: Run tests
run: python -m pytest tests/ -v
11 changes: 4 additions & 7 deletions examples/pgduckdb_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@
import pgembed
import sqlalchemy as sa
from sqlalchemy_utils import database_exists, create_database
import os
from pathlib import Path
import subprocess
import time
import glob
import duckdb

Expand Down Expand Up @@ -55,10 +52,10 @@ def patched_ensure_pgdata_inited(self):
# Only add if not already present
if "pg_duckdb" not in config:
with open(conf_file, "a") as f:
f.write(f"\n# pg_duckdb configuration\n")
f.write(f"shared_preload_libraries = 'pg_duckdb'\n")
f.write("\n# pg_duckdb configuration\n")
f.write("shared_preload_libraries = 'pg_duckdb'\n")
f.write(f"dynamic_library_path = '{pg_duckdb_lib}'\n")
print(f"✓ Configured pg_duckdb in postgresql.conf")
print("✓ Configured pg_duckdb in postgresql.conf")


# Apply the patch
Expand Down Expand Up @@ -491,7 +488,7 @@ def create_migration_function(conn):
dbname = parsed.path.lstrip("/").split("?")[0] or database_name
socket_path = str(pg.pgdata)

print(f" Connecting to PostgreSQL via Unix socket...")
print(" Connecting to PostgreSQL via Unix socket...")

# Attach PostgreSQL using Unix socket
duckdb_conn.execute(
Expand Down
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ dependencies = [
dev = [
"sysv_ipc",
]
# NB: sqlalchemy-utils 0.42.1 (latest) is incompatible with sqlalchemy >= 2.1
# (uses the removed sqlalchemy.orm.attributes.ScalarAttributeImpl),
# so cap sqlalchemy until sqlalchemy-utils supports 2.1.
test = [
"pytest",
"psycopg2-binary",
"sqlalchemy>=2",
"sqlalchemy>=2,<2.1",
"sqlalchemy-utils"
]

Expand All @@ -31,6 +34,9 @@ include = ["pgembed*", "pgembed_pg*"] # package names should match these glob p

[tool.pytest.ini_options]
testpaths = ["tests"]
markers = [
"no_server_required: test runs without built PostgreSQL binaries (lightweight CI)",
]

[tool.cibuildwheel]
test-extras = "test"
Expand Down
11 changes: 10 additions & 1 deletion src/pgembed/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
from ._commands import *
from ._commands import * # noqa: F403
from .postgres_server import PostgresServer, get_server
from pathlib import Path
from typing import Optional
import logging
import importlib.util

__all__ = [
"PostgresServer",
"get_server",
"has_extension",
"list_extensions",
"get_extension_create_name",
"get_extension_path",
]

_logger = logging.getLogger("pgembed")


Expand Down
2 changes: 0 additions & 2 deletions src/pgembed/_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ def _get_pkg_path():
_postgres_binaries_available = POSTGRES_BIN_PATH.exists()

if not _postgres_binaries_available:
import os

_logger = logging.getLogger("pgembed")
_logger.warning(
f"PostgreSQL binaries not found at {POSTGRES_BIN_PATH}. "
Expand Down
13 changes: 6 additions & 7 deletions src/pgembed/postgres_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
find_suitable_socket_dir,
DiskList,
PostmasterInfo,
process_is_running,
)

if platform.system() != "Windows":
Expand All @@ -33,9 +32,9 @@ def _get_command(name: str):
cmd = getattr(_commands, name, None)
if cmd is None:
raise RuntimeError(
f"PostgreSQL binaries not available. "
f"pgembed was installed without PostgreSQL binaries or they were not built. "
f"Run 'make build' in the pgembed source directory."
"PostgreSQL binaries not available. "
"pgembed was installed without PostgreSQL binaries or they were not built. "
"Run 'make build' in the pgembed source directory."
)
return cmd

Expand Down Expand Up @@ -245,14 +244,14 @@ def ensure_postgres_running(self) -> None:
# in Windows, when there is a postmaster.pid, init_ctl seems to return
# but the file is not immediately updated, here we wait until the file shows
# a new running server. see test_stale_postmaster
_logger.info(f"waiting for postmaster info to show a running process")
_logger.info("waiting for postmaster info to show a running process")
pinfo = PostmasterInfo.read_from_pgdata(self.pgdata)
_logger.info(f"running... checking if ready {pinfo=}")
if pinfo is not None and pinfo.is_running() and pinfo.status == "ready":
self._postmaster_info = pinfo
break

_logger.info(f"not ready yet... waiting a bit more...")
_logger.info("not ready yet... waiting a bit more...")
time.sleep(1.0)

_logger.info(f"Now asserting server is running {self._postmaster_info=}")
Expand Down Expand Up @@ -287,7 +286,7 @@ def _cleanup(self) -> None:
pass # somehow the server is already stopped.

if not stopped:
_logger.warning(f"Failed to stop server, killing it instead.")
_logger.warning("Failed to stop server, killing it instead.")
self._postmaster_info.process.terminate()
try:
self._postmaster_info.process.wait(2)
Expand Down
5 changes: 1 addition & 4 deletions src/pgembed/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from pathlib import Path
import typing
from typing import Optional, List, Dict
import subprocess
import json
Expand All @@ -10,7 +9,6 @@
import stat
import psutil
import datetime
import shutil

_logger = logging.getLogger("pgembed")

Expand Down Expand Up @@ -142,14 +140,13 @@ def process_is_running(pid: int) -> bool:


if platform.system() != "Windows":
import pwd

def ensure_user_exists(username: str) -> Optional["pwd.struct_passwd"]:
"""Ensure system user `username` exists.
Returns their pwentry if user exists, otherwise it creates a user through `useradd`.
Assume permissions to add users, eg run as root.
"""
import pwd

try:
entry = pwd.getpwnam(username)
except KeyError:
Expand Down
40 changes: 40 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Pytest configuration for pgembed tests.

Most tests need a running PostgreSQL server backed by the bundled
binaries (built via `make`). Where those binaries have not been built --
e.g. a lightweight PR check or a fresh contributor checkout -- the
server-dependent tests are skipped instead of failing.

Tests that do NOT need a server opt out explicitly with::

@pytest.mark.no_server_required
"""

import importlib.util
from pathlib import Path

import pytest


def _postgres_binaries_available() -> bool:
spec = importlib.util.find_spec("pgembed")
if spec is None or not spec.submodule_search_locations:
return False
bin_dir = Path(spec.submodule_search_locations[0]) / "pginstall" / "bin"
return (bin_dir / "postgres").exists() or (bin_dir / "postgres.exe").exists()


POSTGRES_BINARIES_AVAILABLE = _postgres_binaries_available()

requires_server = pytest.mark.skipif(
not POSTGRES_BINARIES_AVAILABLE,
reason="PostgreSQL binaries not built; run `make` to build them",
)


def pytest_collection_modifyitems(items):
if POSTGRES_BINARIES_AVAILABLE:
return
for item in items:
if "no_server_required" not in item.keywords:
item.add_marker(requires_server)
2 changes: 1 addition & 1 deletion tests/test_pgembed.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import psutil
import platform
import sqlalchemy as sa
import datetime
from sqlalchemy_utils import database_exists, create_database
import logging
import os
Expand Down Expand Up @@ -88,6 +87,7 @@ def _kill_server(pid : Union[int,psutil.Process,None]) -> None:
if proc.is_running():
proc.kill()

@pytest.mark.no_server_required
def test_get_port():
address = '127.0.0.1'
port = find_suitable_port(address)
Expand Down