#!/usr/bin/env python
-"""Convert an async module to a sync module.
+"""Convert async code in the project to sync code.
+
+Note: the version of Python used to run this script affects the output. Please
+use the `async-to-sync.sh` wrapper to use a version consistent with CI checks.
"""
from __future__ import annotations
import os
import sys
+import logging
+import subprocess as sp
from copy import deepcopy
from typing import Any
from pathlib import Path
-from argparse import ArgumentParser, Namespace
+from argparse import ArgumentParser, Namespace, RawDescriptionHelpFormatter
import ast_comments as ast
ast.List = ast_orig.List
ast.Tuple = ast_orig.Tuple
+ALL_INPUTS = """
+ psycopg/psycopg/_copy_async.py
+ psycopg/psycopg/connection_async.py
+ psycopg/psycopg/cursor_async.py
+ psycopg_pool/psycopg_pool/null_pool_async.py
+ psycopg_pool/psycopg_pool/pool_async.py
+ psycopg_pool/psycopg_pool/sched_async.py
+ tests/pool/test_pool_async.py
+ tests/pool/test_pool_common_async.py
+ tests/pool/test_pool_null_async.py
+ tests/pool/test_sched_async.py
+ tests/test_connection_async.py
+ tests/test_copy_async.py
+ tests/test_cursor_async.py
+ tests/test_cursor_client_async.py
+ tests/test_cursor_common_async.py
+ tests/test_cursor_raw_async.py
+ tests/test_cursor_server_async.py
+ tests/test_pipeline_async.py
+ tests/test_prepared_async.py
+ tests/test_tpc_async.py
+ tests/test_transaction_async.py
+""".split()
+
+PROJECT_DIR = Path(__file__).parent.parent
+SCRIPT_NAME = os.path.basename(sys.argv[0])
+
+logger = logging.getLogger()
+logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
+
def main() -> int:
opt = parse_cmdline()
- with opt.filepath.open() as f:
- source = f.read()
+ outputs = []
+ for fpin in opt.inputs:
+ fpout = fpin.parent / fpin.name.replace("_async", "")
+ outputs.append(str(fpout))
+ logger.info("converting %s", fpin)
+ with fpin.open() as f:
+ source = f.read()
+
+ tree = ast.parse(source, filename=str(fpin))
+ tree = async_to_sync(tree, filepath=fpin)
+ output = tree_to_str(tree, fpin)
+
+ with fpout.open("w") as f:
+ print(output, file=f)
- tree = ast.parse(source, filename=str(opt.filepath))
- tree = async_to_sync(tree, filepath=opt.filepath)
- output = tree_to_str(tree, opt.filepath)
+ sp.check_call(["black", "-q", str(fpout)])
- if opt.output:
- with open(opt.output, "w") as f:
- print(output, file=f)
- else:
- print(output)
+ if opt.check:
+ return check(outputs)
+
+ return 0
+
+
+def check(outputs: list[str]) -> int:
+ try:
+ sp.check_call(["git", "diff", "--exit-code"] + outputs)
+ except sp.CalledProcessError:
+ logger.error("sync and async files... out of sync!")
+ return 1
+
+ # Check that all the files to convert are included in the --all list
+ cmdline = ["git", "ls-files", "**.py"]
+ git_pys = sp.check_output(cmdline, cwd=str(PROJECT_DIR)).decode().split()
+
+ cmdline = ["grep", "-l", f"auto-generated by '{SCRIPT_NAME}'"]
+ cmdline += git_pys
+ maybe_conv = sp.check_output(cmdline, cwd=str(PROJECT_DIR)).decode().split()
+ if not maybe_conv:
+ logger.error("no file to check? Maybe this script bitrot?")
+ return 1
+ unk_conv = sorted(
+ set(maybe_conv) - set(fn.replace("_async", "") for fn in ALL_INPUTS)
+ )
+ if unk_conv:
+ logger.error(
+ "files converted by %s but not included in --all list: %s",
+ SCRIPT_NAME,
+ ", ".join(unk_conv),
+ )
+ return 1
return 0
def tree_to_str(tree: ast.AST, filepath: Path) -> str:
rv = f"""\
-# WARNING: this file is auto-generated by '{os.path.basename(sys.argv[0])}'
+# WARNING: this file is auto-generated by '{SCRIPT_NAME}'
# from the original file '{filepath.name}'
# DO NOT CHANGE! Change the original file instead.
"""
def parse_cmdline() -> Namespace:
- parser = ArgumentParser(description=__doc__)
+ parser = ArgumentParser(
+ description=__doc__, formatter_class=RawDescriptionHelpFormatter
+ )
+
+ parser.add_argument(
+ "--check", action="store_true", help="return with error in case of differences"
+ )
parser.add_argument(
- "filepath", metavar="FILE", type=Path, help="the file to process"
+ "--all", action="store_true", help="process all the files of the project"
)
parser.add_argument(
- "output", metavar="OUTPUT", nargs="?", help="file where to write (or stdout)"
+ "inputs",
+ metavar="FILE",
+ nargs="*",
+ type=Path,
+ help="the files to process (if --all is not specified)",
)
+
opt = parser.parse_args()
+ if opt.all and opt.inputs:
+ parser.error("can't specify input files and --all together")
+
+ if opt.all:
+ opt.inputs = [PROJECT_DIR / Path(fn) for fn in ALL_INPUTS]
+
+ if not opt.inputs:
+ parser.error("no input file provided")
+
+ fp: Path
+ for fp in opt.inputs:
+ if not fp.is_file():
+ parser.error("not a file: %s" % fp)
+ if "_async" not in fp.name:
+ parser.error("file should have '_async' in the name: %s" % fp)
return opt
+++ /dev/null
-#!/bin/bash
-
-# Convert all the auto-generated sync files from their async counterparts.
-
-set -euo pipefail
-
-dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-cd "${dir}/.."
-
-function log {
- echo "$@" >&2
-}
-function error {
- # Print an error message and exit.
- log "
-ERROR: $@"
- exit 1
-}
-
-check=
-
-# If --check is used, give an error if files are changed
-# (note: it's not a --dry-run)
-if [[ ${1:-} == '--check' ]]; then
- check=1
- shift
-fi
-
-all_inputs="
- psycopg/psycopg/_copy_async.py
- psycopg/psycopg/connection_async.py
- psycopg/psycopg/cursor_async.py
- psycopg_pool/psycopg_pool/null_pool_async.py
- psycopg_pool/psycopg_pool/pool_async.py
- psycopg_pool/psycopg_pool/sched_async.py
- tests/pool/test_pool_async.py
- tests/pool/test_pool_common_async.py
- tests/pool/test_pool_null_async.py
- tests/pool/test_sched_async.py
- tests/test_connection_async.py
- tests/test_copy_async.py
- tests/test_cursor_async.py
- tests/test_cursor_client_async.py
- tests/test_cursor_common_async.py
- tests/test_cursor_raw_async.py
- tests/test_cursor_server_async.py
- tests/test_pipeline_async.py
- tests/test_prepared_async.py
- tests/test_tpc_async.py
- tests/test_transaction_async.py
-"
-
-# Take other arguments as file names if specified
-if [[ ${1:-} ]]; then
- inputs="$@"
-else
- inputs="$all_inputs"
-fi
-
-
-outputs=""
-
-for async in $inputs; do
- test -f "${async}" || error "file not found: '${async}'"
- sync=${async/_async/}
- log "converting '${async}' -> '${sync}'"
- python "${dir}/async_to_sync.py" ${async} > ${sync}
- black -q ${sync}
- outputs="$outputs ${sync}"
-done
-
-if [[ $check ]]; then
- if ! git diff --exit-code $outputs; then
- error "sync and async files... out of sync!"
- fi
-
- # Verify that all the transformed files are included in this script
- # Note: the 'cd' early in the script ensures that cwd is the project root.
- checked=0
- errors=0
- for fn in $(find psycopg psycopg_pool tests -type f -name \*.py); do
- # Skip files ignored by git (build artifacts)
- ! git status --porcelain --ignored "${fn}" | grep -q '!!' || continue
- # Skip non-auto-generated files
- grep -q "auto-generated by 'async_to_sync.py'" "${fn}" || continue
-
- checked=$(( $checked + 1 ))
- afn=${fn/.py/_async.py}
- if ! grep -q $afn tools/$(basename $0); then
- errors=$(( $errors + 1 ))
- log "file '${fn}' seems converted but not included in '$(basename $0)'"
- fi
- done
- [[ $checked -gt 0 ]] || error "No file found to check. Script bitrot?"
- [[ $errors -eq 0 ]] || error "Some files are not included in async-to-sync conversion."
-fi