flufl.testing-0.8/0000775000076500000240000000000013324417172014415 5ustar barrystaff00000000000000flufl.testing-0.8/LICENSE.txt0000664000076500000240000000106013324411610016224 0ustar barrystaff00000000000000Copyright 2013-2018 Barry Warsaw Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. flufl.testing-0.8/MANIFEST.in0000664000076500000240000000015613131020544016142 0ustar barrystaff00000000000000include *.py MANIFEST.in global-include *.txt *.rst *.po *.mo *.ini exclude .gitignore prune build prune .tox flufl.testing-0.8/NEWS.rst0000664000076500000240000000136013324412016015713 0ustar barrystaff00000000000000=============== flufl.testing =============== Copyright (C) 2013-2018 Barry Warsaw 0.8 (2018-07-20) ================ * Non-``from`` imports can follow ``from``-import from ``__future__`` module. 0.7 (2016-12-14) ================ * Fix minor typo. 0.6 (2016-12-14) ================ * Be sure to declare the namespace package in the setup.py. 0.5 (2016-12-02) ================ * Fix namespace package compatibility. 0.4 (2016-11-30) ================ * More fixes and documentation updates. 0.3 (2016-11-29) ================ * Rename the ``unittest.cfg`` section to ``[flufl.testing]``. * Improve the documentation. 0.2 (2016-11-28) ================ * Re-enable Python 3.4. * Update README. 0.1 (2016-11-17) ================ * Initial release. flufl.testing-0.8/PKG-INFO0000664000076500000240000000105713324417172015515 0ustar barrystaff00000000000000Metadata-Version: 1.2 Name: flufl.testing Version: 0.8 Summary: A small collection of test tool plugins Home-page: https://gitlab.com/warsaw/flufl.testing Maintainer: Barry Warsaw Maintainer-email: barry@python.org License: ASLv2 Download-URL: https://pypi.python.org/pypi/flufl.testing Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Plugins Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python :: 3 flufl.testing-0.8/README.rst0000664000076500000240000001342213324411630016077 0ustar barrystaff00000000000000=============== flufl.testing =============== This is a small collection of test helpers that I use in almost all my packages. Specifically, plugins for the following test tools are provided: * nose2_ * flake8_ Python 3.4 is the minimum supported version. Using test helpers ================== You can use each of the plugins independently. For example, if you use flake8 but you don't use nose2, just enable the flake8 plugin and ignore the rest. flake8 import order plugin -------------------------- This flake8_ plugin enables import order checks as are used in the `GNU Mailman`_ project. Specifically, it enforces the following rules: * Non-``from`` imports must precede ``from``-imports, except import from ``__future__`` module, which should be on the top. * Exactly one line must separate the block of non-``from`` imports from the block of ``from`` imports. * Import exactly one module per non-``from`` import line. * Lines in the non-``from`` import block are sorted by length first, then alphabetically. Dotted imports count toward both restrictions. * Lines in the ``from`` import block are sorted alphabetically. * Multiple names can be imported in a ``from`` import line, but those names must be sorted alphabetically. * Names imported from the same module in a ``from`` import must appear in the same import statement. It's so much easier to see an example:: from __future__ import generator_stop import copy import socket import logging import smtplib from mailman import public from mailman.config import config from mailman.interfaces.mta import IMailTransportAgentDelivery from mailman.mta.connection import Connection from zope.interface import implementer To enable this plugin [#]_, add the following to your ``tox.ini`` or any other `flake8 recognized configuration file`_:: [flake8] enable-extensions = U4 nose2 plugin ------------ The `nose2`_ plugin enables a few helpful things for folks who use that test runner: * Implements better support for doctests, including supporting layers. * Enables sophisticated test pattern matching. * Provides test tracing. * A *log to stderr* flag that you can check. [#]_ * Pluggable doctest setup/teardowns. To enable this plugin, add the following to your ``unittest.cfg`` file, in the ``[unittest]`` section:: plugins = flufl.testing.nose You also need to add this section to ``unittest.cfg``, where ** names the top-level package you want to test:: [flufl.testing] always-on = True package = Now when you run your tests, you can include one or more ``-P`` options, which provide patterns to match your tests against. If given, only tests matching the given pattern are run. This is especially helpful if your test suite is huge. These patterns can match a test name, class, module, or filename, and follow Python's regexp syntax. The following options are also available by setting configuration variables in your ``unittest.cfg`` file, under the ``[flufl.testing]`` section. Doctests ~~~~~~~~ The plugin also provides some useful features for doctests. If you make a directory a package in your source tree (i.e. by adding an `__init__.py`), you can optionally also add specify a `nose2 layer`_ to use for the doctest. Bind the layer object you want to the ``layer`` attribute in the ``__init__.py`` and it will be automatically assigned to the doctest's ``layer`` attribute for nose2 to find. Also for doctests, you can specify the ``setUp()`` and ``tearDown()`` methods you want by adding the following:: setup = my.package.namespace.setup teardown = my.package.other.namespace.teardown The named packages will be imported, with the last path component naming an attribute in the module. This attribute should be a function taking a single argument, in the style used by the stdlib ``doctest.DocFileTest`` class [#]_. You can also name a default layer by setting:: default_layer = my.package.layers.DefaultLayer. This has the same format as the ``setup`` and ``teardown`` settings, except that it should name a class. Pre-test initialization ~~~~~~~~~~~~~~~~~~~~~~~ If you need to do anything before the tests starts, such as initialize database connections or acquire resources, set this:: start_run = my.package.initializer This has the same format as the ``setup`` and ``teardown`` settings, except that it takes a single argument which is the plugin instance. You can use this plugin instance for example to check if the ``-E`` option was given on the command line. This flag sets the ``stderr`` attribute to True on the plugin instance. Tracing ~~~~~~~ If you add this the plugin will also print some additional tracers to stderr for ever test as it starts and stops:: trace = True Author ====== ``flufl.testing`` is Copyright (C) 2013-2018 Barry Warsaw Licensed under the terms of the Apache License, Version 2.0. Project details =============== * Project home: https://gitlab.com/warsaw/flufl.testing * Report bugs at: https://gitlab.com/warsaw/flufl.testing/issues * Code hosting: https://gitlab.com/warsaw/flufl.testing.git * Documentation: https://gitlab.com/warsaw/flufl.testing/tree/master Footnotes ========= .. [#] Note that flake8 3.1 or newer is required. .. [#] It's up to your application to do something with this flag. .. [#] This class is undocumented, so use the doctest_ module source to grok the details. .. _flake8: http://flake8.pycqa.org/en/latest/index.html .. _`GNU Mailman`: http://www.list.org .. _`flake8 recognized configuration file`: http://flake8.pycqa.org/en/latest/user/configuration.html .. _nose2: http://nose2.readthedocs.io/en/latest/index.html .. _`nose2 layer`: http://nose2.readthedocs.io/en/latest/plugins/layers.html .. _doctest: https://docs.python.org/3/library/doctest.html flufl.testing-0.8/flufl/0000775000076500000240000000000013324417172015525 5ustar barrystaff00000000000000flufl.testing-0.8/flufl/__init__.py0000664000076500000240000000007013131020544017620 0ustar barrystaff00000000000000__import__('pkg_resources').declare_namespace(__name__) flufl.testing-0.8/flufl/testing/0000775000076500000240000000000013324417172017202 5ustar barrystaff00000000000000flufl.testing-0.8/flufl/testing/__init__.py0000664000076500000240000000002413324411356021305 0ustar barrystaff00000000000000__version__ = '0.8' flufl.testing-0.8/flufl/testing/imports.py0000664000076500000240000001244613324411700021247 0ustar barrystaff00000000000000"""flake8 plugin.""" from ast import NodeVisitor from collections import namedtuple from enum import Enum class ImportType(Enum): non_from = 0 from_import = 1 ImportRecord = namedtuple('ImportRecord', 'itype lineno colno, module, names') NONFROM_FOLLOWS_FROM = 'U401 Non-from import follows from-import' NONFROM_MULTIPLE_NAMES = 'U402 Multiple names on non-from import' NONFROM_SHORTER_FOLLOWS = 'U403 Shorter non-from import follows longer' NONFROM_ALPHA_UNSORTED = ( 'U404 Same-length non-from imports not sorted alphabetically') NONFROM_EXTRA_BLANK_LINE = ( 'U405 Unexpected blank line since last non-from import') NONFROM_DOTTED_UNSORTED = ( 'U406 Dotted non-from import not sorted alphabetically') FROMIMPORT_MISSING_BLANK_LINE = ( 'U411 Expected one blank line since last non-from import') FROMIMPORT_ALPHA_UNSORTED = 'U412 from-import not sorted alphabetically' FROMIMPORT_MULTIPLE = 'U413 Multiple from-imports of same module' FROMIMPORT_NAMES_UNSORTED = ( 'U414 from-imported names are not sorted alphabetically') class ImportVisitor(NodeVisitor): def __init__(self): self.imports = [] def visit_Import(self, node): if node.col_offset != 0: # Ignore nested imports. return names = [alias.name for alias in node.names] self.imports.append( ImportRecord(ImportType.non_from, node.lineno, node.col_offset, None, names)) def visit_ImportFrom(self, node): if node.col_offset != 0: # Ignore nested imports. return names = [alias.name for alias in node.names] self.imports.append( ImportRecord(ImportType.from_import, node.lineno, node.col_offset, node.module, names)) class ImportOrder: name = 'flufl-import-order' version = '0.2' off_by_default = True def __init__(self, tree, filename): self.tree = tree self.filename = filename def _error(self, record, error): code, space, text = error.partition(' ') return (record.lineno, record.colno, '{} {}'.format(code, text), ImportOrder) def run(self): visitor = ImportVisitor() visitor.visit(self.tree) last_import = None for record in visitor.imports: if last_import is None: last_import = record continue if record.itype is ImportType.non_from: if len(record.names) != 1: yield self._error(record, NONFROM_MULTIPLE_NAMES) if last_import.itype is ImportType.from_import: # If the previous import was a __future__ import, just # ignore the rest of the checks. if last_import.module is '__future__': continue yield self._error(record, NONFROM_FOLLOWS_FROM) # Shorter imports should always precede longer import *except* # when they are dotted imports and everything but the last # path component are the same. In that case, they should be # sorted alphabetically. last_name = last_import.names[0] this_name = record.names[0] if '.' in last_name and '.' in this_name: last_parts = last_name.split('.') this_parts = this_name.split('.') if (last_parts[:-1] == this_parts[:-1] and last_parts[-1] > this_parts[-1]): yield self._error(record, NONFROM_DOTTED_UNSORTED) elif len(last_name) > len(this_name): yield self._error(record, NONFROM_SHORTER_FOLLOWS) # It's also possible that the imports are the same length, in # which case they must be sorted alphabetically. if (len(last_import.names[0]) == len(record.names[0]) and last_import.names[0] > record.names[0]): yield self._error(record, NONFROM_ALPHA_UNSORTED) if last_import.lineno + 1 != record.lineno: yield self._error(record, NONFROM_DOTTED_UNSORTED) else: assert record.itype is ImportType.from_import if (last_import.itype is ImportType.non_from and record.lineno != last_import.lineno + 2): yield self._error(record, FROMIMPORT_MISSING_BLANK_LINE) if last_import.itype is ImportType.non_from: last_import = record continue if last_import.module > record.module: yield self._error(record, FROMIMPORT_ALPHA_UNSORTED) # All imports from the same module should show up in the same # multiline import. if last_import.module == record.module: yield self._error(record, FROMIMPORT_MULTIPLE) # Check the sort order of the imported names. if sorted(record.names) != record.names: yield self._error(record, FROMIMPORT_NAMES_UNSORTED) # How to check for no blank lines between from imports? # Update the last import. last_import = record flufl.testing-0.8/flufl/testing/nose.py0000664000076500000240000001010413324411664020514 0ustar barrystaff00000000000000"""nose2 test infrastructure.""" import os import re import sys import doctest import importlib from nose2.events import Plugin DOT = '.' FLAGS = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | doctest.REPORT_NDIFF def as_object(path): if path is None: return None # mod.ule.object -> import(mod.ule); mod.ule.object module_name, dot, object_name = path.rpartition('.') if dot != '.' or len(object_name) == 0: return None module = importlib.import_module(module_name) return getattr(module, object_name, None) class NosePlugin(Plugin): configSection = 'flufl.testing' def __init__(self): super().__init__() self.patterns = [] self.stderr = False def set_stderr(ignore): # noqa: E306 self.stderr = True self.addArgument(self.patterns, 'P', 'pattern', 'Add a test matching pattern') self.addFlag(set_stderr, 'E', 'stderr', 'Enable stderr logging to sub-runners') # Get the topdir out of the plugin configuration file. self.package = self.config.as_str('package') if self.package is None: raise RuntimeError('flufl.nose2 plugin missing "package" setting') def startTestRun(self, event): callback = as_object(self.config.get('start_run')) if callback is not None: callback(self) def getTestCaseNames(self, event): if len(self.patterns) == 0: # No filter patterns, so everything should be tested. return # Does the pattern match the fully qualified class name? for pattern in self.patterns: full_class_name = '{}.{}'.format( event.testCase.__module__, event.testCase.__name__) if re.search(pattern, full_class_name): # Don't suppress this test class. return names = filter(event.isTestMethod, dir(event.testCase)) for name in names: full_test_name = '{}.{}.{}'.format( event.testCase.__module__, event.testCase.__name__, name) for pattern in self.patterns: if re.search(pattern, full_test_name): break else: event.excludedNames.append(name) def handleFile(self, event): package = importlib.import_module(self.package) path = event.path[len(os.path.dirname(package.__file__))+1:] if len(self.patterns) > 0: for pattern in self.patterns: if re.search(pattern, path): break else: # Skip this doctest. return base, ext = os.path.splitext(path) if ext != '.rst': return # Look to see if the package defines a test layer, otherwise use the # default layer. First turn the file system path into a dotted Python # module path. parent = os.path.dirname(path) dotted = '{}.{}'.format( self.package, DOT.join(parent.split(os.path.sep))) layer = None default_layer = as_object(self.config.get('default_layer')) try: module = importlib.import_module(dotted) except ImportError: layer = default_layer else: layer = getattr(module, 'layer', default_layer) setup = as_object(self.config.get('setup')) teardown = as_object(self.config.get('teardown')) test = doctest.DocFileTest( path, package=self.package, optionflags=FLAGS, setUp=setup, tearDown=teardown) test.layer = layer # Suppress the extra "Doctest: ..." line. test.shortDescription = lambda: None event.extraTests.append(test) def startTest(self, event): if self.config.as_bool('trace', False): print('vvvvv', event.test, file=sys.stderr) def stopTest(self, event): if self.config.as_bool('trace', False): print('^^^^^', event.test, file=sys.stderr) flufl.testing-0.8/flufl.testing.egg-info/0000775000076500000240000000000013324417172020673 5ustar barrystaff00000000000000flufl.testing-0.8/flufl.testing.egg-info/PKG-INFO0000664000076500000240000000105713324417172021773 0ustar barrystaff00000000000000Metadata-Version: 1.2 Name: flufl.testing Version: 0.8 Summary: A small collection of test tool plugins Home-page: https://gitlab.com/warsaw/flufl.testing Maintainer: Barry Warsaw Maintainer-email: barry@python.org License: ASLv2 Download-URL: https://pypi.python.org/pypi/flufl.testing Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Plugins Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python :: 3 flufl.testing-0.8/flufl.testing.egg-info/SOURCES.txt0000664000076500000240000000061213324417172022556 0ustar barrystaff00000000000000LICENSE.txt MANIFEST.in NEWS.rst README.rst setup.py setup_helpers.py flufl/__init__.py flufl.testing.egg-info/PKG-INFO flufl.testing.egg-info/SOURCES.txt flufl.testing.egg-info/dependency_links.txt flufl.testing.egg-info/entry_points.txt flufl.testing.egg-info/namespace_packages.txt flufl.testing.egg-info/top_level.txt flufl/testing/__init__.py flufl/testing/imports.py flufl/testing/nose.pyflufl.testing-0.8/flufl.testing.egg-info/dependency_links.txt0000664000076500000240000000000113324417172024741 0ustar barrystaff00000000000000 flufl.testing-0.8/flufl.testing.egg-info/entry_points.txt0000664000076500000240000000007313324417172024171 0ustar barrystaff00000000000000[flake8.extension] U4 = flufl.testing.imports:ImportOrder flufl.testing-0.8/flufl.testing.egg-info/namespace_packages.txt0000664000076500000240000000000613324417172025222 0ustar barrystaff00000000000000flufl flufl.testing-0.8/flufl.testing.egg-info/top_level.txt0000664000076500000240000000000613324417172023421 0ustar barrystaff00000000000000flufl flufl.testing-0.8/setup.cfg0000664000076500000240000000004613324417172016236 0ustar barrystaff00000000000000[egg_info] tag_build = tag_date = 0 flufl.testing-0.8/setup.py0000664000076500000240000000170713324411715016131 0ustar barrystaff00000000000000from setup_helpers import get_version, require_python from setuptools import setup, find_packages require_python(0x030400f0) __version__ = get_version('flufl/testing/__init__.py') setup( name='flufl.testing', version=__version__, namespace_packages=['flufl'], packages=find_packages(), include_package_data=True, maintainer='Barry Warsaw', maintainer_email='barry@python.org', description='A small collection of test tool plugins', license='ASLv2', url='https://gitlab.com/warsaw/flufl.testing', download_url='https://pypi.python.org/pypi/flufl.testing', entry_points={ 'flake8.extension': ['U4 = flufl.testing.imports:ImportOrder'], }, classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Plugins', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python :: 3', ] ) flufl.testing-0.8/setup_helpers.py0000664000076500000240000001135413131020544017642 0ustar barrystaff00000000000000# Copyright (C) 2016 Barry A. Warsaw # # Licensed under the Apache License, Version 2.0 (the "License"); you may not # use this file except in compliance with the License. You may obtain a copy # of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """setup.py helper functions.""" import os import re import sys DEFAULT_VERSION_RE = re.compile( r'(?P\d+\.\d+(?:\.\d+)?(?:(?:a|b|rc)\d+)?)') EMPTYSTRING = '' __version__ = '2.3' def require_python(minimum): """Require at least a minimum Python version. The version number is expressed in terms of `sys.hexversion`. E.g. to require a minimum of Python 2.6, use:: >>> require_python(0x206000f0) :param minimum: Minimum Python version supported. :type minimum: integer """ if sys.hexversion < minimum: hversion = hex(minimum)[2:] if len(hversion) % 2 != 0: hversion = '0' + hversion split = list(hversion) parts = [] while split: parts.append(int(''.join((split.pop(0), split.pop(0))), 16)) major, minor, micro, release = parts if release == 0xf0: print('Python {0}.{1}.{2} or better is required'.format( major, minor, micro)) else: print('Python {0}.{1}.{2} ({3}) or better is required'.format( major, minor, micro, hex(release)[2:])) sys.exit(1) def get_version(filename, pattern=None): """Extract the __version__ from a file without importing it. While you could get the __version__ by importing the module, the very act of importing can cause unintended consequences. For example, Distribute's automatic 2to3 support will break. Instead, this searches the file for a line that starts with __version__, and extract the version number by regular expression matching. By default, two or three dot-separated digits are recognized, but by passing a pattern parameter, you can recognize just about anything. Use the `version` group name to specify the match group. :param filename: The name of the file to search. :type filename: string :param pattern: Optional alternative regular expression pattern to use. :type pattern: string :return: The version that was extracted. :rtype: string """ if pattern is None: cre = DEFAULT_VERSION_RE else: cre = re.compile(pattern) with open(filename) as fp: for line in fp: if line.startswith('__version__'): mo = cre.search(line) assert mo, 'No valid __version__ string found' return mo.group('version') raise AssertionError('No __version__ assignment found') def find_doctests(start='.', extension='.rst'): """Find separate-file doctests in the package. This is useful for Distribute's automatic 2to3 conversion support. The `setup()` keyword argument `convert_2to3_doctests` requires file names, which may be difficult to track automatically as you add new doctests. :param start: Directory to start searching in (default is cwd) :type start: string :param extension: Doctest file extension (default is .txt) :type extension: string :return: The doctest files found. :rtype: list """ doctests = [] for dirpath, dirnames, filenames in os.walk(start): doctests.extend(os.path.join(dirpath, filename) for filename in filenames if filename.endswith(extension)) return doctests def long_description(*filenames): """Provide a long description.""" res = [''] for filename in filenames: with open(filename) as fp: for line in fp: res.append(' ' + line) res.append('') res.append('\n') return EMPTYSTRING.join(res) def description(filename): """Provide a short description.""" # This ends up in the Summary header for PKG-INFO and it should be a # one-liner. It will get rendered on the package page just below the # package version header but above the long_description, which ironically # gets stuff into the Description header. It should not include reST, so # pick out the first single line after the double header. with open(filename) as fp: for lineno, line in enumerate(fp): if lineno < 3: continue line = line.strip() if len(line) > 0: return line