diff --git a/NEWS b/NEWS index 7f0070cd..09669107 100644 --- a/NEWS +++ b/NEWS @@ -18,6 +18,8 @@ What's new in psycopg 2.4.2 - Trying to execute concurrent operations on the same connection through concurrent green thread results in an error instead of a deadlock. + - Fixed detection of pg_config on Window. Report and fix, plus some + long needed setup.py cleanup by Steve Lacy: thanks! What's new in psycopg 2.4.1 diff --git a/setup.py b/setup.py index f155119d..4840e585 100644 --- a/setup.py +++ b/setup.py @@ -45,21 +45,15 @@ Operating System :: Unix # Note: The setup.py must be compatible with both Python 2 and 3 import os -import os.path import sys import re import subprocess from distutils.core import setup, Extension -from distutils.errors import DistutilsFileError from distutils.command.build_ext import build_ext from distutils.sysconfig import get_python_inc from distutils.ccompiler import get_default_compiler -from distutils.dep_util import newer_group from distutils.util import get_platform -try: - from distutils.msvc9compiler import MSVCCompiler -except ImportError: - MSVCCompiler = None + try: from distutils.command.build_py import build_py_2to3 as build_py except ImportError: @@ -85,21 +79,129 @@ version_flags = ['dt', 'dec'] PLATFORM_IS_WINDOWS = sys.platform.lower().startswith('win') -def get_pg_config(kind, pg_config): - try: - p = subprocess.Popen([pg_config, "--" + kind], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - except OSError: - raise Warning("Unable to find 'pg_config' file in '%s'" % pg_config) - p.stdin.close() - r = p.stdout.readline().strip() - if not r: - raise Warning(p.stderr.readline()) - if not isinstance(r, str): - r = r.decode('ascii') - return r + +class PostgresConfig: + def __init__(self, build_ext): + self.build_ext = build_ext + self.pg_config_exe = self.build_ext.pg_config + if not self.pg_config_exe: + self.pg_config_exe = self.autodetect_pg_config_path() + if self.pg_config_exe is None: + sys.stderr.write("""\ +Error: pg_config executable not found. + +Please add the directory containing pg_config to the PATH +or specify the full executable path with the option: + + python setup.py build_ext --pg-config /path/to/pg_config build ... + +or with the pg_config option in 'setup.cfg'. +""") + sys.exit(1) + + def query(self, attr_name): + """Spawn the pg_config executable, querying for the given config + name, and return the printed value, sanitized. """ + try: + pg_config_process = subprocess.Popen( + [self.pg_config_exe, "--" + attr_name], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + except OSError: + raise Warning("Unable to find 'pg_config' file in '%s'" % + self.pg_config_exe) + pg_config_process.stdin.close() + result = pg_config_process.stdout.readline().strip() + if not result: + raise Warning(pg_config_process.stderr.readline()) + if not isinstance(result, str): + result = result.decode('ascii') + return result + + def find_on_path(self, exename, path_directories=None): + if not path_directories: + path_directories = os.environ['PATH'].split(os.pathsep) + for dir_name in path_directories: + fullpath = os.path.join(dir_name, exename) + if os.path.isfile(fullpath): + return fullpath + return None + + def autodetect_pg_config_path(self): + """Find and return the path to the pg_config executable.""" + if PLATFORM_IS_WINDOWS: + return self.autodetect_pg_config_path_windows() + else: + return self.find_on_path('pg_config') + + def autodetect_pg_config_path_windows(self): + """Attempt several different ways of finding the pg_config + executable on Windows, and return its full path, if found.""" + + # This code only runs if they have not specified a pg_config option + # in the config file or via the commandline. + + # First, check for pg_config.exe on the PATH, and use that if found. + pg_config_exe = self.find_on_path('pg_config.exe') + if pg_config_exe: + return pg_config_exe + + # Now, try looking in the Windows Registry to find a PostgreSQL + # installation, and infer the path from that. + pg_config_exe = self._get_pg_config_from_registry() + if pg_config_exe: + return pg_config_exe + + return None + + def _get_pg_config_from_registry(self): + try: + import winreg + except ImportError: + import _winreg as winreg + + reg = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) + try: + pg_inst_list_key = winreg.OpenKey(reg, + 'SOFTWARE\\PostgreSQL\\Installations') + except EnvironmentError: + # No PostgreSQL installation, as best as we can tell. + return None + + try: + # Determine the name of the first subkey, if any: + try: + first_sub_key_name = winreg.EnumKey(pg_inst_list_key, 0) + except EnvironmentError: + return None + + pg_first_inst_key = winreg.OpenKey(reg, + 'SOFTWARE\\PostgreSQL\\Installations\\' + + first_sub_key_name) + try: + pg_inst_base_dir = winreg.QueryValueEx( + pg_first_inst_key, 'Base Directory')[0] + finally: + winreg.CloseKey(pg_first_inst_key) + + finally: + winreg.CloseKey(pg_inst_list_key) + + pg_config_path = os.path.join( + pg_inst_base_dir, 'bin', 'pg_config.exe') + if not os.path.exists(pg_config_path): + return None + + # Support unicode paths, if this version of Python provides the + # necessary infrastructure: + if sys.version_info[0] < 3 \ + and hasattr(sys, 'getfilesystemencoding'): + pg_config_path = pg_config_path.encode( + sys.getfilesystemencoding()) + + return pg_config_path + class psycopg_build_ext(build_ext): """Conditionally complement the setup.cfg options file. @@ -126,6 +228,9 @@ class psycopg_build_ext(build_ext): boolean_options = build_ext.boolean_options[:] boolean_options.extend(('use-pydatetime', 'have-ssl', 'static-libpq')) + def __init__(self, *args, **kwargs): + build_ext.__init__(self, *args, **kwargs) + def initialize_options(self): build_ext.initialize_options(self) self.use_pg_dll = 1 @@ -136,7 +241,13 @@ class psycopg_build_ext(build_ext): self.static_libpq = static_libpq self.pg_config = None - def get_compiler(self): + def compiler_is_msvc(self): + return self.get_compiler_name().lower().startswith('msvc') + + def compiler_is_mingw(self): + return self.get_compiler_name().lower().startswith('mingw') + + def get_compiler_name(self): """Return the name of the C compiler used to compile extensions. If a compiler was not explicitly set (on the command line, for @@ -153,43 +264,39 @@ class psycopg_build_ext(build_ext): name = get_default_compiler() return name - def get_pg_config(self, kind): - return get_pg_config(kind, self.pg_config) - - def get_export_symbols(self, ext): + def get_export_symbols(self, extension): # Fix MSVC seeing two of the same export symbols. - if self.get_compiler().lower().startswith('msvc'): + if self.compiler_is_msvc(): return [] else: - return build_ext.get_export_symbols(self, ext) + return build_ext.get_export_symbols(self, extension) - def build_extension(self, ext): - build_ext.build_extension(self, ext) + def build_extension(self, extension): + build_ext.build_extension(self, extension) # For Python versions that use MSVC compiler 2008, re-insert the - # manifest into the resulting .pyd file. - if MSVCCompiler and isinstance(self.compiler, MSVCCompiler): + # manifest into the resulting .pyd file. + if self.compiler_is_msvc(): platform = get_platform() # Default to the x86 manifest manifest = '_psycopg.vc9.x86.manifest' if platform == 'win-amd64': manifest = '_psycopg.vc9.amd64.manifest' - self.compiler.spawn(['mt.exe', '-nologo', '-manifest', - os.path.join('psycopg', manifest), - '-outputresource:%s;2' % (os.path.join(self.build_lib, 'psycopg2', '_psycopg.pyd'))]) + self.compiler.spawn( + ['mt.exe', '-nologo', '-manifest', + os.path.join('psycopg', manifest), + '-outputresource:%s;2' % ( + os.path.join(self.build_lib, + 'psycopg2', '_psycopg.pyd'))]) def finalize_win32(self): """Finalize build system configuration on win32 platform.""" - import struct sysVer = sys.version_info[:2] # Add compiler-specific arguments: extra_compiler_args = [] - compiler_name = self.get_compiler().lower() - compiler_is_msvc = compiler_name.startswith('msvc') - compiler_is_mingw = compiler_name.startswith('mingw') - if compiler_is_mingw: + if self.compiler_is_mingw(): # Default MinGW compilation of Python extensions on Windows uses # only -O: extra_compiler_args.append('-O3') @@ -201,23 +308,23 @@ class psycopg_build_ext(build_ext): extra_compiler_args.append('-fno-strict-aliasing') # Force correct C runtime library linkage: - if sysVer <= (2,3): + if sysVer <= (2, 3): # Yes: 'msvcr60', rather than 'msvcrt', is the correct value # on the line below: self.libraries.append('msvcr60') - elif sysVer in ((2,4), (2,5)): + elif sysVer in ((2, 4), (2, 5)): self.libraries.append('msvcr71') # Beyond Python 2.5, we take our chances on the default C runtime # library, because we don't know what compiler those future # versions of Python will use. - for exten in ext: # ext is a global list of Extension objects - exten.extra_compile_args.extend(extra_compiler_args) + for extension in ext: # ext is a global list of Extension objects + extension.extra_compile_args.extend(extra_compiler_args) # End of add-compiler-specific arguments section. self.libraries.append("ws2_32") self.libraries.append("advapi32") - if compiler_is_msvc: + if self.compiler_is_msvc(): # MSVC requires an explicit "libpq" self.libraries.remove("pq") self.libraries.append("secur32") @@ -242,48 +349,39 @@ class psycopg_build_ext(build_ext): def finalize_linux2(self): """Finalize build system configuration on GNU/Linux platform.""" # tell piro that GCC is fine and dandy, but not so MS compilers - for ext in self.extensions: - ext.extra_compile_args.append('-Wdeclaration-after-statement') + for extension in self.extensions: + extension.extra_compile_args.append( + '-Wdeclaration-after-statement') def finalize_options(self): """Complete the build system configuation.""" build_ext.finalize_options(self) - if self.pg_config is None: - self.pg_config = self.autodetect_pg_config_path() - if self.pg_config is None: - sys.stderr.write("""\ -Error: pg_config executable not found. -Please add the directory containing pg_config to the PATH -or specify the full executable path with the option: - - python setup.py build_ext --pg-config /path/to/pg_config build ... - -or with the pg_config option in 'setup.cfg'. -""") - sys.exit(1) + pg_config_helper = PostgresConfig(self) self.include_dirs.append(".") if self.static_libpq: - if not self.link_objects: self.link_objects = [] + if not hasattr(self, 'link_objects'): + self.link_objects = [] self.link_objects.append( - os.path.join(self.get_pg_config("libdir"), "libpq.a")) + os.path.join(pg_config_helper.query("libdir"), "libpq.a")) else: self.libraries.append("pq") try: - self.library_dirs.append(self.get_pg_config("libdir")) - self.include_dirs.append(self.get_pg_config("includedir")) - self.include_dirs.append(self.get_pg_config("includedir-server")) + self.library_dirs.append(pg_config_helper.query("libdir")) + self.include_dirs.append(pg_config_helper.query("includedir")) + self.include_dirs.append(pg_config_helper.query("includedir-server")) try: # Here we take a conservative approach: we suppose that # *at least* PostgreSQL 7.4 is available (this is the only # 7.x series supported by psycopg 2) - pgversion = self.get_pg_config("version").split()[1] + pgversion = pg_config_helper.query("version").split()[1] except: pgversion = "7.4.0" - verre = re.compile(r"(\d+)\.(\d+)(?:(?:\.(\d+))|(devel|(alpha|beta|rc)\d+))") + verre = re.compile( + r"(\d+)\.(\d+)(?:(?:\.(\d+))|(devel|(alpha|beta|rc)\d+))") m = verre.match(pgversion) if m: pgmajor, pgminor, pgpatch = m.group(1, 2, 3) @@ -298,108 +396,21 @@ or with the pg_config option in 'setup.cfg'. define_macros.append(("PG_VERSION_HEX", "0x%02X%02X%02X" % (int(pgmajor), int(pgminor), int(pgpatch)))) except Warning: - w = sys.exc_info()[1] # work around py 2/3 different syntax + w = sys.exc_info()[1] # work around py 2/3 different syntax sys.stderr.write("Error: %s\n" % w) sys.exit(1) if hasattr(self, "finalize_" + sys.platform): getattr(self, "finalize_" + sys.platform)() - def autodetect_pg_config_path(self): - if PLATFORM_IS_WINDOWS: - return self.autodetect_pg_config_path_windows() - else: - return self.autodetect_pg_config_path_posix() - - def autodetect_pg_config_path_posix(self): - exename = 'pg_config' - for dir in os.environ['PATH'].split(os.pathsep): - fn = os.path.join(dir, exename) - if os.path.isfile(fn): - return fn - - def autodetect_pg_config_path_windows(self): - # Find the first PostgreSQL installation listed in the registry and - # return the full path to its pg_config utility. - # - # This autodetection is performed *only* if the following conditions - # hold: - # - # 1) The pg_config utility is not already available on the PATH: - if os.popen('pg_config').close() is None: # .close()->None == success - return None - # 2) The user has not specified any of the following settings in - # setup.cfg: - # - pg_config - # - include_dirs - # - library_dirs - for settingName in ('pg_config', 'include_dirs', 'library_dirs'): - try: - val = parser.get('build_ext', settingName) - except configparser.NoOptionError: - pass - else: - if val.strip() != '': - return None - # end of guard conditions - - try: - import winreg - except ImportError: - import _winreg as winreg - - pg_inst_base_dir = None - pg_config_path = None - - reg = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) - try: - pg_inst_list_key = winreg.OpenKey(reg, - 'SOFTWARE\\PostgreSQL\\Installations' - ) - except EnvironmentError: - pg_inst_list_key = None - - if pg_inst_list_key is not None: - try: - # Determine the name of the first subkey, if any: - try: - first_sub_key_name = winreg.EnumKey(pg_inst_list_key, 0) - except EnvironmentError: - first_sub_key_name = None - - if first_sub_key_name is not None: - pg_first_inst_key = winreg.OpenKey(reg, - 'SOFTWARE\\PostgreSQL\\Installations\\' - + first_sub_key_name - ) - try: - pg_inst_base_dir = winreg.QueryValueEx( - pg_first_inst_key, 'Base Directory' - )[0] - finally: - winreg.CloseKey(pg_first_inst_key) - finally: - winreg.CloseKey(pg_inst_list_key) - - if pg_inst_base_dir and os.path.exists(pg_inst_base_dir): - pg_config_path = os.path.join(pg_inst_base_dir, 'bin', - 'pg_config.exe' - ) - # Support unicode paths, if this version of Python provides the - # necessary infrastructure: - if sys.version_info[0] < 3 \ - and hasattr(sys, 'getfilesystemencoding'): - pg_config_path = pg_config_path.encode( - sys.getfilesystemencoding()) - - return pg_config_path # let's start with macro definitions (the ones not already in setup.cfg) define_macros = [] include_dirs = [] # gather information to build the extension module -ext = [] ; data_files = [] +ext = [] +data_files = [] # sources @@ -452,7 +463,7 @@ else: if os.path.exists(mxincludedir): # Build the support for mx: we will check at runtime if it can be imported include_dirs.append(mxincludedir) - define_macros.append(('HAVE_MXDATETIME','1')) + define_macros.append(('HAVE_MXDATETIME', '1')) sources.append('adapter_mxdatetime.c') depends.extend(['adapter_mxdatetime.h', 'typecast_mxdatetime.c']) have_mxdatetime = True @@ -460,18 +471,21 @@ if os.path.exists(mxincludedir): # now decide which package will be the default for date/time typecasts if have_pydatetime and (use_pydatetime or not have_mxdatetime): - define_macros.append(('PSYCOPG_DEFAULT_PYDATETIME','1')) + define_macros.append(('PSYCOPG_DEFAULT_PYDATETIME', '1')) elif have_mxdatetime: - define_macros.append(('PSYCOPG_DEFAULT_MXDATETIME','1')) + define_macros.append(('PSYCOPG_DEFAULT_MXDATETIME', '1')) else: - def e(msg): - sys.stderr.write("error: " + msg + "\n") - e("psycopg requires a datetime module:") - e(" mx.DateTime module not found") - e(" python datetime module not found") - e("Note that psycopg needs the module headers and not just the module") - e("itself. If you installed Python or mx.DateTime from a binary package") - e("you probably need to install its companion -dev or -devel package.") + error_message = """\ +psycopg requires a datetime module: + mx.DateTime module not found + python datetime module not found + +Note that psycopg needs the module headers and not just the module +itself. If you installed Python or mx.DateTime from a binary package +you probably need to install its companion -dev or -devel package.""" + + for line in error_message.split("\n"): + sys.stderr.write("error: " + line) sys.exit(1) # generate a nice version string to avoid confusion when users report bugs @@ -485,9 +499,9 @@ else: PSYCOPG_VERSION_EX = PSYCOPG_VERSION if not PLATFORM_IS_WINDOWS: - define_macros.append(('PSYCOPG_VERSION', '"'+PSYCOPG_VERSION_EX+'"')) + define_macros.append(('PSYCOPG_VERSION', '"' + PSYCOPG_VERSION_EX + '"')) else: - define_macros.append(('PSYCOPG_VERSION', '\\"'+PSYCOPG_VERSION_EX+'\\"')) + define_macros.append(('PSYCOPG_VERSION', '\\"' + PSYCOPG_VERSION_EX + '\\"')) if parser.has_option('build_ext', 'have_ssl'): have_ssl = int(parser.get('build_ext', 'have_ssl')) @@ -525,17 +539,16 @@ setup(name="psycopg2", author="Federico Di Gregorio", author_email="fog@initd.org", url="http://initd.org/psycopg/", - download_url = download_url, + download_url=download_url, license="GPL with exceptions or ZPL", - platforms = ["any"], + platforms=["any"], description=__doc__.split("\n")[0], long_description="\n".join(__doc__.split("\n")[2:]), classifiers=[x for x in classifiers.split("\n") if x], data_files=data_files, - package_dir={'psycopg2':'lib', 'psycopg2.tests': 'tests'}, + package_dir={'psycopg2': 'lib', 'psycopg2.tests': 'tests'}, packages=['psycopg2', 'psycopg2.tests'], cmdclass={ 'build_ext': psycopg_build_ext, 'build_py': build_py, }, ext_modules=ext) -