flufl.testing-0.7/0000775000175000017500000000000013024261754014432 5ustar barrybarry00000000000000flufl.testing-0.7/PKG-INFO0000664000175000017500000000104713024261754015531 0ustar barrybarry00000000000000Metadata-Version: 1.1 Name: flufl.testing Version: 0.7 Summary: A small collection of test tool plugins Home-page: https://gitlab.com/warsaw/flufl.testing Author: Barry Warsaw Author-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.7/flufl/0000775000175000017500000000000013024261754015542 5ustar barrybarry00000000000000flufl.testing-0.7/flufl/testing/0000775000175000017500000000000013024261754017217 5ustar barrybarry00000000000000flufl.testing-0.7/flufl/testing/imports.py0000664000175000017500000001317113016645470021273 0ustar barrybarry00000000000000# 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. 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: 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.7/flufl/testing/nose.py0000664000175000017500000001121613017612543020534 0ustar barrybarry00000000000000# Copyright (C) 2013-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. """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.7/flufl/testing/__init__.py0000664000175000017500000000002413024261702021315 0ustar barrybarry00000000000000__version__ = '0.7' flufl.testing-0.7/flufl/__init__.py0000664000175000017500000000007013020446227017644 0ustar barrybarry00000000000000__import__('pkg_resources').declare_namespace(__name__) flufl.testing-0.7/setup_helpers.py0000664000175000017500000001135413016645470017674 0ustar barrybarry00000000000000# 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 flufl.testing-0.7/flufl.testing.egg-info/0000775000175000017500000000000013024261754020710 5ustar barrybarry00000000000000flufl.testing-0.7/flufl.testing.egg-info/SOURCES.txt0000664000175000017500000000061213024261754022573 0ustar barrybarry00000000000000LICENSE.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.7/flufl.testing.egg-info/PKG-INFO0000664000175000017500000000104713024261754022007 0ustar barrybarry00000000000000Metadata-Version: 1.1 Name: flufl.testing Version: 0.7 Summary: A small collection of test tool plugins Home-page: https://gitlab.com/warsaw/flufl.testing Author: Barry Warsaw Author-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.7/flufl.testing.egg-info/entry_points.txt0000664000175000017500000000007313024261754024206 0ustar barrybarry00000000000000[flake8.extension] U4 = flufl.testing.imports:ImportOrder flufl.testing-0.7/flufl.testing.egg-info/namespace_packages.txt0000664000175000017500000000000613024261754025237 0ustar barrybarry00000000000000flufl flufl.testing-0.7/flufl.testing.egg-info/dependency_links.txt0000664000175000017500000000000113024261754024756 0ustar barrybarry00000000000000 flufl.testing-0.7/flufl.testing.egg-info/top_level.txt0000664000175000017500000000000613024261754023436 0ustar barrybarry00000000000000flufl flufl.testing-0.7/setup.cfg0000664000175000017500000000007313024261754016253 0ustar barrybarry00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 flufl.testing-0.7/MANIFEST.in0000664000175000017500000000015613016645470016174 0ustar barrybarry00000000000000include *.py MANIFEST.in global-include *.txt *.rst *.po *.mo *.ini exclude .gitignore prune build prune .tox flufl.testing-0.7/LICENSE.txt0000664000175000017500000000105313024072374016252 0ustar barrybarry00000000000000Copyright 2016 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.7/NEWS.rst0000664000175000017500000000117513024261663015743 0ustar barrybarry00000000000000=============== flufl.testing =============== Copyright (C) 2016 Barry A. Warsaw 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.7/README.rst0000664000175000017500000001323513017470316016123 0ustar barrybarry00000000000000=============== 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. * 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:: 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-2016 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.7/setup.py0000664000175000017500000000301413024072415016134 0ustar barrybarry00000000000000# 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. from 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', ] )