From af9408b592f3e5aa1d29938febea459391437c24 Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Sat, 18 Feb 2017 17:06:19 -0500 Subject: [PATCH 1/4] include clauses can now be ANDed or ORed --- skipnose/skipnose.py | 90 ++++++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 33 deletions(-) diff --git a/skipnose/skipnose.py b/skipnose/skipnose.py index f62773a..208f88b 100644 --- a/skipnose/skipnose.py +++ b/skipnose/skipnose.py @@ -53,12 +53,14 @@ def options(self, parser, env=os.environ): """ truth = ('true', '1', 'on') skipnose_on = env.get(self.env_opt, 'False').lower() in truth - skip_include = filter( - bool, re.split(r'[,;:]', env.get(self.env_include_opt, '')) - ) - skip_exclude = filter( + + skip_include = list(filter( + bool, re.split(r'[,;]', env.get(self.env_include_opt, '')) + )) + + skip_exclude = list(filter( bool, re.split(r'[,;:]', env.get(self.env_exclude_opt, '')) - ) + )) parser.add_option( '--with-skipnose', @@ -66,8 +68,8 @@ def options(self, parser, env=os.environ): default=skipnose_on, dest='skipnose', help='skipnose: enable skipnose nose plugin ' - '(alternatively, set ${}=1)' - ''.format(self.env_opt) + '(alternatively, set ${env}=1)' + ''.format(env=self.env_opt) ) parser.add_option( '--skipnose-debug', @@ -79,24 +81,26 @@ def options(self, parser, env=os.environ): parser.add_option( '--skipnose-include', action='append', - default=list(skip_include), + default=skip_include, dest='skipnose_include', help='skipnose: which directory to include in tests ' 'using glob syntax.' - 'can be specified multiple times. ' - '(alternatively, set ${} as [,;:] delimited string)' - ''.format(self.env_include_opt) + 'Specifying multiple times will AND the clauses. ' + 'Single parameter ":" delimited clauses will be ORed. ' + 'Alternatively, set ${env} as [,;] delimited string for ' + 'AND and [:] for OR.' + ''.format(env=self.env_include_opt) ) parser.add_option( '--skipnose-exclude', action='append', - default=list(skip_exclude), + default=skip_exclude, dest='skipnose_exclude', help='skipnose: which directory to exclude in tests ' 'using glob syntax.' - 'can be specified multiple times. ' - '(alternatively, set ${} as [,;:] delimited string)' - ''.format(self.env_exclude_opt) + 'Can be specified multiple times. ' + '(alternatively, set ${env} as [,;:] delimited string)' + ''.format(env=self.env_exclude_opt) ) parser.add_option( '--skipnose-skip-tests', @@ -121,7 +125,10 @@ def configure(self, options, conf): if options.skipnose: self.enabled = True self.debug = options.skipnose_debug - self.skipnose_include = options.skipnose_include + self.skipnose_include = list(map( + lambda i: i.split(':'), + options.skipnose_include + )) self.skipnose_exclude = options.skipnose_exclude if options.skipnose_skip_tests: @@ -169,27 +176,18 @@ def wantDirectory(self, dirname): basename = os.path.basename(dirname) if self.skipnose_include: - # check all subfolders to see if any of them match - # if yes, then this parent folder should be included - # so that nose can get to the subfolder - subfolders = map(os.path.basename, - list(walk_subfolders(dirname)) + [dirname]) - want = any(map(lambda i: fnmatch.filter(subfolders, i), - self.skipnose_include)) - - # if directory is not wanted then there is a possibility - # it is a subfolder of a wanted directory so - # check against parent folder patterns - if not want: - parts = dirname.split(os.sep) - want = any(map(lambda i: fnmatch.filter(parts, i), - self.skipnose_include)) + want = all(map( + lambda i: self._want_directory_by_includes(dirname, i), + self.skipnose_include + )) if self.skipnose_exclude and want is not False: # exclude the folder if the folder path # matches any of the exclude patterns - want = not any(map(lambda i: fnmatch.fnmatch(basename, i), - self.skipnose_exclude)) + want = not any(map( + lambda i: fnmatch.fnmatch(basename, i), + self.skipnose_exclude + )) if self.debug: if not want: @@ -200,6 +198,32 @@ def wantDirectory(self, dirname): # normalize boolean to only ``False`` or ``None`` return False if want is False else None + @staticmethod + def _want_directory_by_includes(dirname, includes): + # check all subfolders to see if any of them match + # if yes, then this parent folder should be included + # so that nose can get to the subfolder + subfolders = map( + os.path.basename, + list(walk_subfolders(dirname)) + [dirname] + ) + want = any(map( + lambda i: fnmatch.filter(subfolders, i), + includes + )) + + # if directory is not wanted then there is a possibility + # it is a subfolder of a wanted directory so + # check against parent folder patterns + if not want: + parts = dirname.split(os.sep) + want = any(map( + lambda i: fnmatch.filter(parts, i), + includes + )) + + return want + def startTest(self, test): """ Skip tests when skipnose_skip_tests is provided From 06057994a8e19c08d332095e78ac73ce2316cf5f Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Sat, 18 Feb 2017 17:06:38 -0500 Subject: [PATCH 2/4] adjusted tests for the boolean include clauses --- tests/tests_skipnose.py | 217 +++++++++++++++++++++++++++------------- 1 file changed, 146 insertions(+), 71 deletions(-) diff --git a/tests/tests_skipnose.py b/tests/tests_skipnose.py index 3e9bc90..eaa548c 100644 --- a/tests/tests_skipnose.py +++ b/tests/tests_skipnose.py @@ -88,8 +88,8 @@ def test_configure(self, mock_path_exists, mock_sys_exit): """ mock_options = mock.MagicMock( skipnose_debug=mock.sentinel.debug, - skipnose_include=mock.sentinel.include, - skipnose_exclude=mock.sentinel.exclude, + skipnose_include=['a', 'b:c'], + skipnose_exclude=['x', 'y'], skipnose_skip_tests='foo.json', ) mock_path_exists.return_value = True @@ -104,8 +104,8 @@ def test_configure(self, mock_path_exists, mock_sys_exit): self.assertTrue(self.plugin.enabled) self.assertEqual(self.plugin.debug, mock.sentinel.debug) - self.assertEqual(self.plugin.skipnose_include, mock.sentinel.include) - self.assertEqual(self.plugin.skipnose_exclude, mock.sentinel.exclude) + self.assertEqual(self.plugin.skipnose_include, [['a'], ['b', 'c']]) + self.assertEqual(self.plugin.skipnose_exclude, ['x', 'y']) self.assertEqual(self.plugin.skipnose_skip_tests, ['one', 'two']) mock_open.assert_called_once_with('foo.json', 'rb') @@ -118,8 +118,8 @@ def test_configure_error(self, mock_path_exists, mock_sys_exit): """ mock_options = mock.MagicMock( skipnose_debug=mock.sentinel.debug, - skipnose_include=mock.sentinel.include, - skipnose_exclude=mock.sentinel.exclude, + skipnose_include=['a', 'b:c'], + skipnose_exclude=['x', 'y'], skipnose_skip_tests='foo.data', ) mock_path_exists.return_value = False @@ -132,8 +132,8 @@ def test_configure_error(self, mock_path_exists, mock_sys_exit): self.assertTrue(self.plugin.enabled) self.assertEqual(self.plugin.debug, mock.sentinel.debug) - self.assertEqual(self.plugin.skipnose_include, mock.sentinel.include) - self.assertEqual(self.plugin.skipnose_exclude, mock.sentinel.exclude) + self.assertEqual(self.plugin.skipnose_include, [['a'], ['b', 'c']]) + self.assertEqual(self.plugin.skipnose_exclude, ['x', 'y']) self.assertIsNone(self.plugin.skipnose_skip_tests) self.assertFalse(mock_open.called) mock_sys_exit.assert_called_once_with(1) @@ -149,106 +149,181 @@ def setUp(self): super(TestSkipNose, self).setUp() self.plugin = SkipNose() self.plugin.debug = True - self.test_paths = ( - ('/test', ('api-parent', 'foo-parent',)), - ('/test/bar/cat/one', ('non-api',)), - ('/test/bar/cat/one/subone', ('non-api',)), - ('/test/bar/cat/two', ('non-api',)), - ('/test/bar/dog/one', ('api-parent',)), - ('/test/bar/dog/one/api', ('api',)), - ('/test/bar/dog/one/api/subapi', ('api-child',)), - ('/test/bar/dog/one/api/subapi/moreapi', ('api-child',)), - ('/test/bar/dog/one/api/subapi/evenmoreapi', ('api-child',)), - ('/test/bar/dog/one/api/subapi/evenmoreapi/api', ('api-child',)), - ('/test/foo', ('api-parent-foo', 'foo')), - ('/test/foo/api', ('api', 'foo-child')), - ('/test/foo/api/subapi', ('api-child', 'foo-child')), - ('/test/foo/api/subapi/moreapi', ('api-child', 'foo-child')), - ('/test/foo/api/subapi/evenmoreapi', ('api-child', 'foo-child')), - ('/test/foo/api/subsubapi', ('api-child', 'foo-child')), - ('/test/foo/api/subsubapi/toomuchapi', ('api-child', 'foo-child')), - ('/test/foo/nonapi', ('non-api', 'foo-child')), - ('/test/foo/nonapi/folderone', ('non-api', 'foo-child')), - ('/test/foo/nonapi/foldertwo', ('non-api', 'foo-child')), - ('/test/foo/nonapi/foldertwo/morestuff', ('non-api', 'foo-child')), - ('/test/foo/nonapi/foldertwo/toomuch', ('non-api', 'foo-child')), - ) + self.test_paths = [ + '/test', + '/test/bar/cat/one', + '/test/bar/cat/one/subone', + '/test/bar/cat/two', + '/test/bar/dog/one', + '/test/bar/dog/one/api', + '/test/bar/dog/one/api/subapi', + '/test/bar/dog/one/api/subapi/moreapi', + '/test/bar/dog/one/api/subapi/evenmoreapi', + '/test/bar/dog/one/api/subapi/evenmoreapi/api', + '/test/foo', + '/test/foo/api', + '/test/foo/api/subapi', + '/test/foo/api/subapi/moreapi', + '/test/foo/api/subapi/evenmoreapi', + '/test/foo/api/subsubapi', + '/test/foo/api/subsubapi/toomuchapi', + '/test/foo/nonapi', + '/test/foo/nonapi/folderone', + '/test/foo/nonapi/foldertwo', + '/test/foo/nonapi/foldertwo/morestuff', + '/test/foo/nonapi/foldertwo/toomuch', + ] def _mock_walk_subfolders(self, path): """ Function to be provided to mock.side_effect to replace walk_subfolders functionality to use test paths. """ - paths = list(map(lambda i: i[0], self.test_paths)) + paths = list(map(lambda i: i, self.test_paths)) index = paths.index(path) if len(paths) > index + 1: return filter(lambda i: i.startswith(path), paths[index:]) else: return [] + def _test_paths(self, valid): + for path in self.test_paths: + expected = None if path in valid else False + actual = self.plugin.wantDirectory(path) + self.assertEqual(actual, expected, + '{} != {} for {}'.format(actual, expected, path)) + @mock.patch('skipnose.skipnose.walk_subfolders') def test_want_directory_include(self, mock_walk_subfolders): """ Test wantDirectory with include parameter """ mock_walk_subfolders.side_effect = self._mock_walk_subfolders + valid = [ + '/test', + '/test/bar/dog/one', + '/test/bar/dog/one/api', + '/test/bar/dog/one/api/subapi', + '/test/bar/dog/one/api/subapi/moreapi', + '/test/bar/dog/one/api/subapi/evenmoreapi', + '/test/bar/dog/one/api/subapi/evenmoreapi/api', + '/test/foo', + '/test/foo/api', + '/test/foo/api/subapi', + '/test/foo/api/subapi/moreapi', + '/test/foo/api/subapi/evenmoreapi', + '/test/foo/api/subsubapi', + '/test/foo/api/subsubapi/toomuchapi', + ] - self.plugin.skipnose_include = ['api'] - for path, properties in self.test_paths: - accepted = ('api-parent', 'api-parent-foo', 'api', 'api-child') - expected = (None if any(map(lambda i: i in properties, accepted)) - else False) - actual = self.plugin.wantDirectory(path) - self.assertEqual(actual, expected, - '{} != {} for {}'.format(actual, expected, path)) + self.plugin.skipnose_include = [['api']] + self._test_paths(valid) @mock.patch('skipnose.skipnose.walk_subfolders') - def test_want_directory_include_multiple(self, mock_walk_subfolders): + def test_want_directory_include_multiple_or(self, mock_walk_subfolders): """ - Test wantDirectory with multiple include parameters + Test wantDirectory with multiple include OR parameters """ mock_walk_subfolders.side_effect = self._mock_walk_subfolders + valid = [ + '/test', + '/test/bar/dog/one', + '/test/bar/dog/one/api', + '/test/bar/dog/one/api/subapi', + '/test/bar/dog/one/api/subapi/moreapi', + '/test/bar/dog/one/api/subapi/evenmoreapi', + '/test/bar/dog/one/api/subapi/evenmoreapi/api', + '/test/foo', + '/test/foo/api', + '/test/foo/api/subapi', + '/test/foo/api/subapi/moreapi', + '/test/foo/api/subapi/evenmoreapi', + '/test/foo/api/subsubapi', + '/test/foo/api/subsubapi/toomuchapi', + '/test/foo/nonapi', + '/test/foo/nonapi/folderone', + '/test/foo/nonapi/foldertwo', + '/test/foo/nonapi/foldertwo/morestuff', + '/test/foo/nonapi/foldertwo/toomuch', + ] - self.plugin.skipnose_include = ['api', 'foo'] - for path, properties in self.test_paths: - accepted = ('api-parent', 'api', 'api-child', 'foo', 'foo-child') - expected = (None if any(map(lambda i: i in properties, accepted)) - else False) - actual = self.plugin.wantDirectory(path) - self.assertEqual(actual, expected, - '{} != {} for {}'.format(actual, expected, path)) + self.plugin.skipnose_include = [['api', 'foo']] + self._test_paths(valid) + + @mock.patch('skipnose.skipnose.walk_subfolders') + def test_want_directory_include_multiple_and(self, mock_walk_subfolders): + """ + Test wantDirectory with multiple include OR parameters + """ + mock_walk_subfolders.side_effect = self._mock_walk_subfolders + valid = [ + '/test', + '/test/foo', + '/test/foo/api', + '/test/foo/api/subapi', + '/test/foo/api/subapi/moreapi', + '/test/foo/api/subapi/evenmoreapi', + '/test/foo/api/subsubapi', + '/test/foo/api/subsubapi/toomuchapi', + ] + + self.plugin.skipnose_include = [['api'], ['foo']] + self._test_paths(valid) def test_want_directory_exclude(self): """ Test wantDirectory with exclude parameter """ + valid = [ + '/test', + '/test/bar/cat/one', + '/test/bar/cat/one/subone', + '/test/bar/cat/two', + '/test/bar/dog/one', + '/test/bar/dog/one/api/subapi', # implicitly skipped by walk + '/test/bar/dog/one/api/subapi/moreapi', # noqa implicitly skipped by walk + '/test/bar/dog/one/api/subapi/evenmoreapi', # noqa implicitly skipped by walk + '/test/foo', + '/test/foo/api/subapi', # implicitly skipped by walk + '/test/foo/api/subapi/moreapi', # implicitly skipped by walk + '/test/foo/api/subapi/evenmoreapi', # implicitly skipped by walk + '/test/foo/api/subsubapi', # implicitly skipped by walk + '/test/foo/api/subsubapi/toomuchapi', # implicitly skipped by walk + '/test/foo/nonapi', + '/test/foo/nonapi/folderone', + '/test/foo/nonapi/foldertwo', + '/test/foo/nonapi/foldertwo/morestuff', + '/test/foo/nonapi/foldertwo/toomuch', + ] self.plugin.skipnose_exclude = ['api'] - for path, properties in self.test_paths: - # exclude subfolders where parent should be rejected - if 'api-child' in properties: - continue - accepted = ('api-parent', 'api-parent-foo', 'non-api') - expected = (None if any(map(lambda i: i in properties, accepted)) - else False) - actual = self.plugin.wantDirectory(path) - self.assertEqual(actual, expected, - '{} != {} for {}'.format(actual, expected, path)) + self._test_paths(valid) def test_want_directory_exclude_multiple(self): """ Test wantDirectory with multiple exclude parameter """ + valid = [ + '/test', + '/test/bar/cat/one', + '/test/bar/cat/one/subone', + '/test/bar/cat/two', + '/test/bar/dog/one', + '/test/bar/dog/one/api/subapi', # implicitly skipped by walk + '/test/bar/dog/one/api/subapi/moreapi', # noqa implicitly skipped by walk + '/test/bar/dog/one/api/subapi/evenmoreapi', # noqa implicitly skipped by walk + '/test/foo/api/subapi', # implicitly skipped by walk + '/test/foo/api/subapi/moreapi', # implicitly skipped by walk + '/test/foo/api/subapi/evenmoreapi', # implicitly skipped by walk + '/test/foo/api/subsubapi', # implicitly skipped by walk + '/test/foo/api/subsubapi/toomuchapi', # implicitly skipped by walk + '/test/foo/nonapi', # implicitly skipped by walk + '/test/foo/nonapi/folderone', # implicitly skipped by walk + '/test/foo/nonapi/foldertwo', # implicitly skipped by walk + '/test/foo/nonapi/foldertwo/morestuff', # noqa implicitly skipped by walk + '/test/foo/nonapi/foldertwo/toomuch', # implicitly skipped by walk + ] self.plugin.skipnose_exclude = ['api', 'foo'] - for path, properties in self.test_paths: - # exclude subfolders where parent should be rejected - if 'api-child' in properties or 'foo-child' in properties: - continue - accepted = ('api-parent', 'non-api', 'foo-parent') - expected = (None if any(map(lambda i: i in properties, accepted)) - else False) - actual = self.plugin.wantDirectory(path) - self.assertEqual(actual, expected, - '{} != {} for {}'.format(actual, expected, path)) + self._test_paths(valid) def test_start_test_no_tests_to_skip(self): self.plugin.skipnose_skip_tests = None From a49d5e713420c02d27b6082e2b4e1a07f2b5ad2f Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Sat, 18 Feb 2017 17:08:59 -0500 Subject: [PATCH 3/4] added setup.cfg with flake8 config --- Makefile | 2 +- setup.cfg | 2 ++ setup.py | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 setup.cfg diff --git a/Makefile b/Makefile index 35cad69..8fffe0b 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ clean-test-all: clean-test rm -rf .tox/ lint: - flake8 skipnose tests + flake8 . test: nosetests ${NOSE_FLAGS} tests/ diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..347c9ca --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[flake8] +exclude = .tox diff --git a/setup.py b/setup.py index 3ddb22d..40b5d81 100644 --- a/setup.py +++ b/setup.py @@ -27,8 +27,8 @@ def read(fname): test_requirements = ( - read('requirements.txt').splitlines() - + read('requirements-dev.txt').splitlines()[1:] + read('requirements.txt').splitlines() + + read('requirements-dev.txt').splitlines()[1:] ) From 0fea04fc4ebdae52411e4844159c907bce69eab9 Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Sat, 18 Feb 2017 21:28:40 -0500 Subject: [PATCH 4/4] bumped version to 0.3. also updated README and HISTORY --- HISTORY.rst | 13 +++++++++++++ README.rst | 33 ++++++++++++++++++++++++++++++--- skipnose/__init__.py | 2 +- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index c0de6e6..4684188 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,19 @@ History ------- +0.3.0 (2017-02-18) +~~~~~~~~~~~~~~~~~~ + +* **New** ``--skipnose-include`` clauses now can either be ANDed or ORed. + + ANDed example:: + + --skipnose-include=foo --skipnose-include=bar + + ORed example:: + + --skipnose-include=foo:bar + 0.2.0 (2016-02-24) ~~~~~~~~~~~~~~~~~~ diff --git a/README.rst b/README.rst index ae475df..14d6966 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ tests are executed. Using ----- -Plugin adds 3 configuration options to nosetests: +Plugin adds 3 configuration options to ``nosetests``: ``--with-skipnose`` Required option to enable ``skipnose`` plugin functionality. @@ -55,14 +55,41 @@ Plugin adds 3 configuration options to nosetests: ``--skipnose-include`` This option specifies using glob pattern the only folders nosetests - should run. This option can also be provided multiple times and - alternatively can be provided as a ``[,;:]``-delimited + should run. This option can also be provided multiple times. + Each given parameter clause will be ANDed. + Within each parameter ``:`` delimited clauses will be ORed. + In addition this parameter can be provided as a ``[,;:]``-delimited (``[,;]`` for AND and ``[:]`` for OR) ``NOSE_SKIPNOSE_INCLUDE`` environment variable:: $ nosetests --with-skipnose --skipnose-include=foo3 --skipnose-include=sub2foo? $ # is equivalent to $ NOSE_SKIPNOSE_INCLUDE=foo3,sub2foo? nosetests --with-skipnose + which would result in only the following folders being included in the tests:: + + tests/ + foo1/ + sub1foo1/ + ... + sub2foo1/ + ... + foo2/ + sub1foo2/ + ... + sub2foo2/ + ... + foo3/ <= only this will run + sub1foo3/ + ... + sub2foo3/ <= only this will run + ... + + Here is an example when clauses are ORed:: + + $ nosetests --with-skipnose --skipnose-include=foo3:sub2foo? + $ # is equivalent to + $ NOSE_SKIPNOSE_INCLUDE=foo3:sub2foo? nosetests --with-skipnose + which would result in only the following folders being included in the tests:: tests/ diff --git a/skipnose/__init__.py b/skipnose/__init__.py index fce6ff7..1e3e991 100644 --- a/skipnose/__init__.py +++ b/skipnose/__init__.py @@ -6,4 +6,4 @@ __author__ = 'Miroslav Shubernetskiy' __email__ = 'miroslav@miki725.com' -__version__ = '0.2.0' +__version__ = '0.3.0'