diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 19a76dc..f8338dd 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,20 +1,20 @@ [bumpversion] -current_version = 2.0.0 +current_version = 2.1.0 commit = True tag = True message = "Release {new_version}" [bumpversion:file:setup.py] search = version='{current_version}' replace = version='{new_version}' [bumpversion:file:src/pytest_postgresql/__init__.py] [bumpversion:file:README.rst] [bumpversion:file:CHANGES.rst] search = unreleased ------- replace = {new_version} ------- diff --git a/.travis.yml b/.travis.yml index a4b826c..7def26d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,48 +1,58 @@ dist: xenial language: python python: - 3.5 - 3.6 - 3.7 +- 3.8-dev - pypy3 # blocklist branches branches: except: - requires-io-master + - /^dependabot.*$/ install: - pip install -r requirements-test.txt -- pip install -e .[tests] coveralls wheel +- pip install coveralls wheel script: -- py.test -n 0 --cov src/pytest_postgresql +- py.test -n 0 after_success: - coveralls jobs: include: - stage: xdist python: 3.7 - script: py.test -n 1 --cov src/pytest_postgresql tests + script: py.test -n 1 - stage: linters python: 3.7 install: - pip install -r requirements-lint.txt - - pip install .[tests] psycopg2-binary script: - pycodestyle - pydocstyle - pylint pytest_postgresql tests - pyroma . after_success: skip + - stage: osx + language: generic + os: osx + before_install: + - pip3 install virtualenv + - virtualenv venv -p python3 + - source venv/bin/activate + script: + - py.test -n 0 - stage: deploy python: 3.7 if: tag IS present script: skip deploy: provider: pypi user: thearoom password: secure: FAN5dMk+ktvFdfZX6OjKy9+XWwbTrJcZ4OrV6LVKNyZdsVRi0+iE6opSQXH8HjO6DCXsyHkZDD8a6f81y/Cc3j6QsRItnJwjQllu4dNce5LYHZNA/sQ9O8mgC9+DrPWzPYRlMkSgG9eVH3tI8UX1P7Wh4yuJLbQbNkWw8ZX7j+HSwZtYLPhP2uBp7xMF5rYO+9PcIA/I0QI0AkfRQtYtwSp3QAjKUVWnWXnUQOILey5wP+3ENYVojKmYSocmOtbKUUNfExgZIep8gsZXx60fuBQLRSG0XhDxud51nGKvzNegqQoN5Mpt61VslT7trS/D6zOzMCUTqNwbg/gMrebU5IMZm5ijJSKkvw5Cl30Yc5+cZ0o7N9NbepBjlEX3ByDUnDk7bfSRHukJw9lG5+mBHLi+aJ9+ZPEqUcIHuYzv6+yvjlGPkETqlxkCunU6HzwrsD5QdYwqcY4PLHf7Onx3I2Vjg4XxZO27BDbhvpOTF4SaAk45ALi9y10Gu7FSswp44/l5k54Ur1/JPhSmFGIO8XZJZ7mLGftextYLEmQuai7IKmu46rZ3ffzEpIk0lP5xi2NWyQvD9DHEJSMKxc4koVKb72lLbl69aVp+6vKRH4VtJ1E5/Hybrxe9bbz+upQ4xRdXjRAml4xAnGeBcH5hf2owbijYuoU26qOHKl68Ej0= on: tags: true all_branches: true repo: ClearcodeHQ/pytest-postgresql distributions: bdist_wheel diff --git a/CHANGES.rst b/CHANGES.rst index 7476eed..46e5728 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,87 +1,94 @@ CHANGELOG ========= +2.1.0 +------- + +- [enhancement] Gather helper functions maintaining postgresql database in DatabaseJanitor class. +- [deprecate] Deprecate ``init_postgresql_database`` in favour of ``DatabaseJanitor.init`` +- [deprecate] Deprecate ``drop_postgresql_database`` in favour of ``DatabaseJanitor.drop`` + 2.0.0 ------- - [feature] Drop support for python 2.7. From now on, only support python 3.5 and up - [feature] Ability to configure database name through plugin options - [enhancement] Use tmpdir_factory. Drop ``logsdir`` parameter - [ehnancement] Support only Postgresql 9.0 and up - [bugfix] Always start postgresql with LC_ALL, LC_TYPE and LANG set to C.UTF-8. It makes postgresql start in english. 1.4.1 ------- - [bugfix] Allow creating test databse with hyphens 1.4.0 ------- - [enhancements] Ability to configure additional options for postgresql process and connection - [bugfix] - removed hard dependency on ``psycopg2``, allowing any of its alternative packages, like ``psycopg2-binary``, to be used. - [maintenance] Drop support for python 3.4 and use 3.7 instead 1.3.4 ------- - [bugfix] properly detect if executor running and clean after executor is being stopped .. note:: Previously if a test failed, there was a possibility of the executor being removed when python was closing, causing it to print ignored errors on already unloaded modules. 1.3.3 ------- - [enhancement] use executor's context manager to start/stop postrgesql server in a fixture 1.3.2 ------- - [bugfix] version regexp to correctly catch postgresql 10 1.3.1 ------- - [enhancement] explicitly turn off logging_collector 1.3.0 ------- - [feature] pypy compatibility 1.2.0 ------- - [bugfix] - disallow connection to database before it gets dropped. .. note:: Otherwise it caused random test subprocess to connect again and this the drop was unsucessfull which resulted in many more test failes on setup. - [cleanup] - removed path.py dependency 1.1.1 ------- - [bugfix] - Fixing the default pg_ctl path creation 1.1.0 ------- - [feature] - migrate usage of getfuncargvalue to getfixturevalue. require at least pytest 3.0.0 1.0.0 ------- - create command line and pytest.ini configuration options for postgresql starting parameters - create command line and pytest.ini configuration options for postgresql username - make the port random by default - create command line and pytest.ini configuration options for executable - create command line and pytest.ini configuration options for host - create command line and pytest.ini configuration options for port - Extracted code from pytest-dbfixtures diff --git a/README.rst b/README.rst index e087ca9..8cf81d9 100644 --- a/README.rst +++ b/README.rst @@ -1,170 +1,203 @@ pytest-postgresql ================= .. image:: https://img.shields.io/pypi/v/pytest-postgresql.svg :target: https://pypi.python.org/pypi/pytest-postgresql/ :alt: Latest PyPI version .. image:: https://img.shields.io/pypi/wheel/pytest-postgresql.svg :target: https://pypi.python.org/pypi/pytest-postgresql/ :alt: Wheel Status .. image:: https://img.shields.io/pypi/pyversions/pytest-postgresql.svg :target: https://pypi.python.org/pypi/pytest-postgresql/ :alt: Supported Python Versions .. image:: https://img.shields.io/pypi/l/pytest-postgresql.svg :target: https://pypi.python.org/pypi/pytest-postgresql/ :alt: License Package status -------------- -.. image:: https://travis-ci.org/ClearcodeHQ/pytest-postgresql.svg?branch=v2.0.0 +.. image:: https://travis-ci.org/ClearcodeHQ/pytest-postgresql.svg?branch=v2.1.0 :target: https://travis-ci.org/ClearcodeHQ/pytest-postgresql :alt: Tests -.. image:: https://coveralls.io/repos/ClearcodeHQ/pytest-postgresql/badge.png?branch=v2.0.0 - :target: https://coveralls.io/r/ClearcodeHQ/pytest-postgresql?branch=v2.0.0 +.. image:: https://coveralls.io/repos/ClearcodeHQ/pytest-postgresql/badge.png?branch=v2.1.0 + :target: https://coveralls.io/r/ClearcodeHQ/pytest-postgresql?branch=v2.1.0 :alt: Coverage Status -.. image:: https://requires.io/github/ClearcodeHQ/pytest-postgresql/requirements.svg?tag=v2.0.0 - :target: https://requires.io/github/ClearcodeHQ/pytest-postgresql/requirements/?tag=v2.0.0 +.. image:: https://requires.io/github/ClearcodeHQ/pytest-postgresql/requirements.svg?tag=v2.1.0 + :target: https://requires.io/github/ClearcodeHQ/pytest-postgresql/requirements/?tag=v2.1.0 :alt: Requirements Status What is this? ============= This is a pytest plugin, that enables you to test your code that relies on a running PostgreSQL Database. It allows you to specify fixtures for PostgreSQL process and client. How to use ========== .. warning:: Tested on PostgreSQL versions > 9.x. See tests for more details. Install with: .. code-block:: sh pip install pytest-postgresql You will also need to install ``psycopg2``, or one of its alternative packagings such as ``psycopg2-binary`` (pre-compiled wheels) or ``psycopg2cffi`` (CFFI based, useful on PyPy). Plugin contains two fixtures: * **postgresql** - it's a client fixture that has functional scope. After each test it ends all leftover connections, and drops test database from PostgreSQL ensuring repeatability. * **postgresql_proc** - session scoped fixture, that starts PostgreSQL instance at it's first use and stops at the end of the tests. Simply include one of these fixtures into your tests fixture list. You can also create additional postgresql client and process fixtures if you'd need to: .. code-block:: python from pytest_postgresql import factories postgresql_my_proc = factories.postgresql_proc( port=None, unixsocketdir='/var/run') postgresql_my = factories.postgresql('postgresql_my_proc') .. note:: Each PostgreSQL process fixture can be configured in a different way than the others through the fixture factory arguments. Configuration ============= You can define your settings in three ways, it's fixture factory argument, command line option and pytest.ini configuration option. You can pick which you prefer, but remember that these settings are handled in the following order: * ``Fixture factory argument`` * ``Command line option`` * ``Configuration option in your pytest.ini file`` .. list-table:: Configuration options :header-rows: 1 * - PostgreSQL option - Fixture factory argument - Command line option - pytest.ini option - Default * - Path to executable - executable - --postgresql-exec - postgresql_exec - /usr/lib/postgresql/9.1/bin/pg_ctl * - host - host - --postgresql-host - postgresql_host - 127.0.0.1 * - port - port - --postgresql-port - postgresql_port - random * - postgresql user - user - --postgresql-user - postgresql_user - postgres * - Starting parameters - startparams - --postgresql-startparams - postgresql_startparams - -w * - Log filename's prefix - logsprefix - --postgresql-logsprefix - postgresql_logsprefix - * - Location for unixsockets - unixsocket - --postgresql-unixsocketdir - postgresql_unixsocketdir - $TMPDIR * - Database name - db_name - --postgresql-dbname - postgresql_dbname - test Example usage: * pass it as an argument in your own fixture .. code-block:: python postgresql_proc = factories.postgresql_proc( port=8888) * use ``--postgresql-port`` command line option when you run your tests .. code-block:: py.test tests --postgresql-port=8888 * specify your port as ``postgresql_port`` in your ``pytest.ini`` file. To do so, put a line like the following under the ``[pytest]`` section of your ``pytest.ini``: .. code-block:: ini [pytest] postgresql_port = 8888 +Maintaining database state outside of the fixtures +-------------------------------------------------- + +It is possible and appears it's used in other libraries for tests, +to maintain database state with the use of the ``pytest-postgresql`` database +managing functionality: + +For this import DatabaseJanitor and use it's init and drop methods: + + +.. code-block:: python + + from pytest_postgresql.factories import DatabaseJanitor + + # variable definition + + janitor = DatabaseJanitor(user, host, port, db_name, version) + janitor.init() + # your code, or yield + janitor.drop() + # at this moment you'll have clean database step + +or use it as a context manager: + +.. code-block:: python + + from pytest_postgresql.factories import DatabaseJanitor + + # variable definition + + with DatabaseJanitor(user, host, port, db_name, version): + # do something here + Package resources ----------------- * Bug tracker: https://github.com/ClearcodeHQ/pytest-postgresql/issues diff --git a/docs/source/conf.py b/docs/source/conf.py index 28a13fd..fcd61b2 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,312 +1,312 @@ # -*- coding: utf-8 -*- # Copyright (C) 2016 by Clearcode # and associates (see AUTHORS). # This file is part of pytest-postgresql. # pytest-postgresql is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # pytest-postgresql is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public License # along with pytest-postgresql. If not, see . # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'pytest-postgresql' basename = ''.join(project.split('.')) author = 'Clearcode - The A Room' copyright = '2016, ' + author # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. from pypt import __version__ # The full version, including alpha/beta/rc tags. release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". +# " version documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = basename + 'doc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', basename + '.tex', project + ' Documentation', author, 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', basename, project + ' Documentation', [author], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', basename, project + ' Documentation', author, basename, 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # -- Options for Epub output --------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project epub_author = author epub_publisher = author epub_copyright = '2016, ' + author # The language of the text. It defaults to the language option # or en if the language is not set. # epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. # epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. # epub_identifier = '' # A unique identification for the text. # epub_uid = '' # A tuple containing the cover image and cover page html template filenames. # epub_cover = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. # epub_pre_files = [] # HTML files shat should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. # epub_post_files = [] # A list of files that should not be packed into the epub file. # epub_exclude_files = [] # The depth of the table of contents in toc.ncx. # epub_tocdepth = 3 # Allow duplicate toc entries. # epub_tocdup = True # Autodoc configuration: autoclass_content = 'both' autodoc_default_flags = ['members', 'show-inheritance'] # Intersphinx configuration intersphinx_mapping = {'python': ('http://docs.python.org/', None)} diff --git a/pytest.ini b/pytest.ini index a34198b..143010f 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,5 @@ [pytest] -addopts = --max-slave-restart=0 --showlocals --verbose -postgresql_exec = /usr/lib/postgresql/9.6/bin/pg_ctl +addopts = --max-slave-restart=0 --showlocals --verbose --cov src/pytest_postgresql --cov tests +postgresql_exec = /usr/lib/postgresql/10/bin/pg_ctl testpaths = tests xfail_strict = true diff --git a/requirements-lint.txt b/requirements-lint.txt index f997e17..8090ff0 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -1,6 +1,7 @@ # linters pycodestyle==2.5.0 -pydocstyle==4.0.0 +pydocstyle==4.0.1 pylint==2.3.1 pygments pyroma==2.5 +-r requirements-test.txt \ No newline at end of file diff --git a/requirements-test.txt b/requirements-test.txt index b77975c..3267457 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,9 +1,11 @@ # test runs requirements (versions we'll be testing against) - automatically updated by requires.io pip>=9 # minimum installation requirements setuptools>=21 # minimum installation requirements coverage==4.5.4 # pytest-cov -pytest==5.0.1 +pytest==5.1.2 psycopg2-binary==2.8.3; platform_python_implementation != "PyPy" psycopg2cffi==2.8.1; platform_python_implementation == "PyPy" port-for==0.4 -mirakuru==2.0.1 +mirakuru==2.1.0; python_version>'3.5' +mirakuru<2.1.0; python_version<='3.5' +-e .[tests] \ No newline at end of file diff --git a/setup.py b/setup.py index 1b4ffea..af36c6c 100644 --- a/setup.py +++ b/setup.py @@ -1,102 +1,102 @@ # -*- coding: utf-8 -*- # Copyright (C) 2016 by Clearcode # and associates (see AUTHORS). # This file is part of pytest-postgresql. # pytest-postgresql is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # pytest-postgresql is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public License # along with pytest-postgresql. If not, see . """pytest-postgresql setup.py module.""" import os from setuptools import setup, find_packages here = os.path.dirname(__file__) def read(fname): """ Read given file's content. :param str fname: file name :returns: file contents :rtype: str """ return open(os.path.join(here, fname)).read() requirements = [ 'pytest>=3.0.0', 'port-for', 'mirakuru>=2.0.0' ] test_requires = [ 'pytest-cov==2.7.1', 'pytest-xdist==1.29.0', ] extras_require = { 'docs': ['sphinx'], 'tests': test_requires, } setup_requires = [ 'setuptools>=21', 'pip>=9' ] setup( name='pytest-postgresql', - version='2.0.0', + version='2.1.0', description='Postgresql fixtures and fixture factories for Pytest.', long_description=( read('README.rst') + '\n\n' + read('CHANGES.rst') ), keywords='tests py.test pytest fixture postgresql', author='Clearcode - The A Room', author_email='thearoom@clearcode.cc', url='https://github.com/ClearcodeHQ/pytest-postgresql', license='LGPLv3+', python_requires='>=3.5', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: ' 'GNU Lesser General Public License v3 or later (LGPLv3+)', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3 :: Only', 'Topic :: Software Development :: Libraries :: Python Modules', ], package_dir={'': 'src'}, packages=find_packages('src'), install_requires=requirements, tests_require=test_requires, setup_requires=setup_requires, test_suite='tests', entry_points={ 'pytest11': [ 'pytest_postgresql = pytest_postgresql.plugin' ]}, include_package_data=True, zip_safe=False, extras_require=extras_require, ) diff --git a/src/pytest_postgresql/__init__.py b/src/pytest_postgresql/__init__.py index 8870fb1..eaf8b8e 100644 --- a/src/pytest_postgresql/__init__.py +++ b/src/pytest_postgresql/__init__.py @@ -1,27 +1,27 @@ # -*- coding: utf-8 -*- # Copyright (C) 2016 by Clearcode # and associates (see AUTHORS). # This file is part of pytest-postgresql. # pytest-postgresql is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # pytest-postgresql is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public License # along with pytest-postgresql. If not, see . """Main module for pytest-postgresql.""" try: import psycopg2cffi.compat # pylint:disable=import-error except ImportError: pass else: psycopg2cffi.compat.register() -__version__ = '2.0.0' +__version__ = '2.1.0' diff --git a/src/pytest_postgresql/factories.py b/src/pytest_postgresql/factories.py index 3dd0a3a..db02a12 100644 --- a/src/pytest_postgresql/factories.py +++ b/src/pytest_postgresql/factories.py @@ -1,238 +1,214 @@ # Copyright (C) 2013-2016 by Clearcode # and associates (see AUTHORS). # This file is part of pytest-dbfixtures. # pytest-dbfixtures is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # pytest-dbfixtures is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public License # along with pytest-dbfixtures. If not, see . """Fixture factories for postgresql fixtures.""" import os.path import platform import subprocess from tempfile import gettempdir +from warnings import warn import pytest -from pkg_resources import parse_version - -try: - import psycopg2 -except ImportError: - psycopg2 = False +from pytest_postgresql.janitor import DatabaseJanitor, psycopg2 from pytest_postgresql.executor import PostgreSQLExecutor from pytest_postgresql.port import get_port def get_config(request): """Return a dictionary with config options.""" config = {} options = [ 'exec', 'host', 'port', 'user', 'options', 'startparams', 'logsprefix', 'unixsocketdir', 'dbname' ] for option in options: option_name = 'postgresql_' + option conf = request.config.getoption(option_name) or \ request.config.getini(option_name) config[option] = conf return config def init_postgresql_database(user, host, port, db_name): """ Create database in postgresql. :param str user: postgresql username :param str host: postgresql host :param str port: postgresql port :param str db_name: database name """ - conn = psycopg2.connect(user=user, host=host, port=port) - conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) - cur = conn.cursor() - cur.execute('CREATE DATABASE "{}";'.format(db_name)) - cur.close() - conn.close() + warn( + 'init_postgresql_database is deprecated, ' + 'use DatabaseJanitor.init istead.', + DeprecationWarning + ) + DatabaseJanitor(user, host, port, db_name, 0.0).init() def drop_postgresql_database(user, host, port, db_name, version): """ Drop databse in postgresql. :param str user: postgresql username :param str host: postgresql host :param str port: postgresql port :param str db_name: database name :param packaging.version.Version version: postgresql version number """ - conn = psycopg2.connect(user=user, host=host, port=port) - conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) - cur = conn.cursor() - # We cannot drop the database while there are connections to it, so we - # terminate all connections first while not allowing new connections. - if version >= parse_version('9.2'): - pid_column = 'pid' - else: - pid_column = 'procpid' - cur.execute( - 'UPDATE pg_database SET datallowconn=false WHERE datname = %s;', - (db_name,)) - cur.execute( - 'SELECT pg_terminate_backend(pg_stat_activity.{})' - 'FROM pg_stat_activity WHERE pg_stat_activity.datname = %s;'.format( - pid_column), - (db_name,)) - cur.execute('DROP DATABASE IF EXISTS "{}";'.format(db_name)) - cur.close() - conn.close() + warn( + 'drop_postgresql_database is deprecated, ' + 'use DatabaseJanitor.drop istead.', + DeprecationWarning + ) + DatabaseJanitor(user, host, port, db_name, version).init() def postgresql_proc( executable=None, host=None, port=-1, user=None, options='', startparams=None, unixsocketdir=None, logs_prefix='', ): """ Postgresql process factory. :param str executable: path to postgresql_ctl :param str host: hostname :param str|int|tuple|set|list port: exact port (e.g. '8000', 8000) randomly selected port (None) - any random available port -1 - command line or pytest.ini configured port [(2000,3000)] or (2000,3000) - random available port from a given range [{4002,4003}] or {4002,4003} - random of 4002 or 4003 ports [(2000,3000), {4002,4003}] - random of given range and set :param str user: postgresql username :param str startparams: postgresql starting parameters :param str unixsocketdir: directory to create postgresql's unixsockets :param str logs_prefix: prefix for log filename :rtype: func :returns: function which makes a postgresql process """ @pytest.fixture(scope='session') def postgresql_proc_fixture(request, tmpdir_factory): """ Process fixture for PostgreSQL. :param FixtureRequest request: fixture request object :rtype: pytest_dbfixtures.executors.TCPExecutor :returns: tcp executor """ config = get_config(request) postgresql_ctl = executable or config['exec'] # check if that executable exists, as it's no on system PATH # only replace if executable isn't passed manually if not os.path.exists(postgresql_ctl) and executable is None: pg_bindir = subprocess.check_output( ['pg_config', '--bindir'], universal_newlines=True ).strip() postgresql_ctl = os.path.join(pg_bindir, 'pg_ctl') pg_host = host or config['host'] pg_port = get_port(port) or get_port(config['port']) datadir = os.path.join( gettempdir(), 'postgresqldata.{}'.format(pg_port)) pg_user = user or config['user'] pg_options = options or config['options'] pg_unixsocketdir = unixsocketdir or config['unixsocketdir'] pg_startparams = startparams or config['startparams'] logfile_path = tmpdir_factory.mktemp("data").join( '{prefix}postgresql.{port}.log'.format( prefix=logs_prefix, port=pg_port ) ) if platform.system() == 'FreeBSD': with (datadir / 'pg_hba.conf').open(mode='a') as conf_file: conf_file.write('host all all 0.0.0.0/0 trust\n') postgresql_executor = PostgreSQLExecutor( executable=postgresql_ctl, host=pg_host, port=pg_port, user=pg_user, options=pg_options, datadir=datadir, unixsocketdir=pg_unixsocketdir, logfile=logfile_path, startparams=pg_startparams, ) # start server with postgresql_executor: postgresql_executor.wait_for_postgres() yield postgresql_executor return postgresql_proc_fixture def postgresql(process_fixture_name, db_name=None): """ Return connection fixture factory for PostgreSQL. :param str process_fixture_name: name of the process fixture :param str db_name: database name :rtype: func :returns: function which makes a connection to postgresql """ @pytest.fixture def postgresql_factory(request): """ Fixture factory for PostgreSQL. :param FixtureRequest request: fixture request object :rtype: psycopg2.connection :returns: postgresql client """ config = get_config(request) if not psycopg2: raise ImportError( 'No module named psycopg2. Please install either ' 'psycopg2 or psycopg2-binary package for CPython ' 'or psycopg2cffi for Pypy.' ) proc_fixture = request.getfixturevalue(process_fixture_name) # _, config = try_import('psycopg2', request) pg_host = proc_fixture.host pg_port = proc_fixture.port pg_user = proc_fixture.user pg_options = proc_fixture.options pg_db = db_name or config['dbname'] - init_postgresql_database(pg_user, pg_host, pg_port, pg_db) - connection = psycopg2.connect( - dbname=pg_db, - user=pg_user, - host=pg_host, - port=pg_port, - options=pg_options - ) - - def drop_database(): - connection.close() - drop_postgresql_database( + with DatabaseJanitor( pg_user, pg_host, pg_port, pg_db, proc_fixture.version + ): + connection = psycopg2.connect( + dbname=pg_db, + user=pg_user, + host=pg_host, + port=pg_port, + options=pg_options ) - - request.addfinalizer(drop_database) - - return connection + yield connection + connection.close() return postgresql_factory __all__ = ('postgresql', 'postgresql_proc') diff --git a/src/pytest_postgresql/janitor.py b/src/pytest_postgresql/janitor.py new file mode 100644 index 0000000..ea4e591 --- /dev/null +++ b/src/pytest_postgresql/janitor.py @@ -0,0 +1,97 @@ +"""Database Janitor.""" +from contextlib import contextmanager +from types import TracebackType +from typing import TypeVar, Union, Optional, Type, Any + +from pkg_resources import parse_version +Version = type(parse_version('1')) # pylint:disable=invalid-name + +try: + import psycopg2 + try: + from psycopg2._psycopg import cursor + except ImportError: + from psycopg2cffi._impl.cursor import Cursor as cursor +except ImportError: + psycopg2 = False + # if there's no postgres, just go with the flow. + cursor = Any # pylint:disable=invalid-name + +DatabaseJanitorType = TypeVar("DatabaseJanitorType", bound="DatabaseJanitor") + + +class DatabaseJanitor: + """Manage database state for specific tasks.""" + + def __init__( + self, + user: str, + host: str, + port: str, + db_name: str, + version: Union[str, float, Version] + ) -> None: + """ + Initialize janitor. + + :param user: postgresql username + :param host: postgresql host + :param port: postgresql port + :param db_name: database name + :param version: postgresql version number + """ + self.user = user + self.host = host + self.port = port + self.db_name = db_name + if not isinstance(version, Version): + self.version = parse_version(str(version)) + else: + self.version = version + + def init(self) -> None: + """Create database in postgresql.""" + with self.cursor() as cur: + cur.execute('CREATE DATABASE "{}";'.format(self.db_name)) + + def drop(self) -> None: + """Drop database in postgresql.""" + # We cannot drop the database while there are connections to it, so we + # terminate all connections first while not allowing new connections. + if self.version >= parse_version('9.2'): + pid_column = 'pid' + else: + pid_column = 'procpid' + with self.cursor() as cur: + cur.execute( + 'UPDATE pg_database SET datallowconn=false WHERE datname = %s;', + (self.db_name,)) + cur.execute( + 'SELECT pg_terminate_backend(pg_stat_activity.{})' + 'FROM pg_stat_activity ' + 'WHERE pg_stat_activity.datname = %s;'.format(pid_column), + (self.db_name,)) + cur.execute('DROP DATABASE IF EXISTS "{}";'.format(self.db_name)) + + @contextmanager + def cursor(self) -> cursor: + """Return postgresql cursor.""" + conn = psycopg2.connect(user=self.user, host=self.host, port=self.port) + conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) + cur = conn.cursor() + try: + yield cur + finally: + cur.close() + conn.close() + + def __enter__(self: DatabaseJanitorType) -> DatabaseJanitorType: + self.init() + return self + + def __exit__( + self: DatabaseJanitorType, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType]) -> None: + self.drop() diff --git a/tests/conftest.py b/tests/conftest.py index f0b3d54..5670a6a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,21 +1,21 @@ """Tests main conftest file.""" from pytest_postgresql import factories PG_CTL = '/usr/lib/postgresql/{ver}/bin/pg_ctl' # pylint:disable=invalid-name postgresql92 = factories.postgresql_proc(PG_CTL.format(ver='9.2'), port=None) postgresql93 = factories.postgresql_proc(PG_CTL.format(ver='9.3'), port=None) postgresql94 = factories.postgresql_proc(PG_CTL.format(ver='9.4'), port=None) postgresql95 = factories.postgresql_proc(PG_CTL.format(ver='9.5'), port=None) postgresql96 = factories.postgresql_proc(PG_CTL.format(ver='9.6'), port=None) postgresql10 = factories.postgresql_proc(PG_CTL.format(ver='10'), port=None) -postgresql101 = factories.postgresql_proc(PG_CTL.format(ver='10.1'), port=None) +postgresql11 = factories.postgresql_proc(PG_CTL.format(ver='11'), port=None) postgresql_proc2 = factories.postgresql_proc(port=9876) postgresql2 = factories.postgresql('postgresql_proc2', db_name='test-db') postgresql_rand_proc = factories.postgresql_proc(port=None) postgresql_rand = factories.postgresql('postgresql_rand_proc') # pylint:enable=invalid-name diff --git a/tests/test_janitor.py b/tests/test_janitor.py new file mode 100644 index 0000000..6ec558d --- /dev/null +++ b/tests/test_janitor.py @@ -0,0 +1,14 @@ +"""Database Janitor tests.""" +import pytest +from pkg_resources import parse_version + +from pytest_postgresql.janitor import DatabaseJanitor + +VERSION = parse_version('9.2') + + +@pytest.mark.parametrize('version', (VERSION, 9.2, '9.2')) +def test_version_cast(version): + """Test that version is cast to Version object.""" + janitor = DatabaseJanitor(None, None, None, None, version) + assert janitor.version == VERSION diff --git a/tests/test_postgresql.py b/tests/test_postgresql.py index 4086a8a..83f33b8 100644 --- a/tests/test_postgresql.py +++ b/tests/test_postgresql.py @@ -1,59 +1,68 @@ """All tests for pytest-postgresql.""" +import platform import psycopg2 import pytest QUERY = "CREATE TABLE test (id serial PRIMARY KEY, num integer, data varchar);" +@pytest.mark.skipif( + platform.system() == 'Darwin', + reason='These fixtures are only for linux' +) @pytest.mark.parametrize('postgres', ( 'postgresql94', 'postgresql95', 'postgresql96', 'postgresql10', pytest.param('postgresql11', marks=pytest.mark.xfail), )) def test_postgresql_proc(request, postgres): """Test different postgresql versions.""" postgresql_proc = request.getfixturevalue(postgres) assert postgresql_proc.running() is True def test_main_postgres(postgresql): """Check main postgresql fixture.""" cur = postgresql.cursor() cur.execute(QUERY) postgresql.commit() cur.close() def test_two_postgreses(postgresql, postgresql2): """Check two postgresql fixtures on one test.""" cur = postgresql.cursor() cur.execute(QUERY) postgresql.commit() cur.close() cur = postgresql2.cursor() cur.execute(QUERY) postgresql2.commit() cur.close() def test_rand_postgres_port(postgresql_rand): """Check if postgres fixture can be started on random port.""" assert postgresql_rand.status == psycopg2.extensions.STATUS_READY @pytest.mark.parametrize('_', range(2)) def test_postgres_terminate_connection( postgresql, _): """ Test that connections are terminated between tests. And check that only one exists at a time. """ cur = postgresql.cursor() - cur.execute('SELECT * FROM pg_stat_activity;') - assert len(cur.fetchall()) == 1, 'there is always only one connection' + cur.execute( + 'SELECT * FROM pg_stat_activity ' + 'WHERE backend_type = \'client backend\';' + ) + existing_connections = cur.fetchall() + assert len(existing_connections) == 1, 'there is always only one connection' cur.close()