From 8c0f6a1bf75b8faa63d80e5fd3d6c39eed0d4fd8 Mon Sep 17 00:00:00 2001 From: Lily Li Date: Wed, 12 Dec 2018 15:06:22 -0800 Subject: [PATCH 1/8] semver checker: do not return error msg on new args with default values --- .../package_crawler_static.py | 6 +++-- .../compatibility_lib/semver_checker.py | 3 +-- .../compatibility_lib/test_semver_checker.py | 7 ++++++ .../testpkgs/optional_args/0.1.0/main.py | 23 +++++++++++++++++++ .../testpkgs/optional_args/0.2.0/main.py | 23 +++++++++++++++++++ 5 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 compatibility_lib/compatibility_lib/testpkgs/optional_args/0.1.0/main.py create mode 100644 compatibility_lib/compatibility_lib/testpkgs/optional_args/0.2.0/main.py diff --git a/compatibility_lib/compatibility_lib/package_crawler_static.py b/compatibility_lib/compatibility_lib/package_crawler_static.py index fe478309..b7aa574e 100644 --- a/compatibility_lib/compatibility_lib/package_crawler_static.py +++ b/compatibility_lib/compatibility_lib/package_crawler_static.py @@ -219,9 +219,11 @@ def _get_function_info(functions): def _get_args(node): - """returns a list of args""" + """returns a list of non-default args, ignoring args with defaults""" args = [] - for arg in node.args: + num_args = len(node.args) - len(node.defaults) + for i in range(num_args): + arg = node.args[i] if isinstance(arg, ast.arg): args.append(arg.arg) elif isinstance(arg, ast.Name): diff --git a/compatibility_lib/compatibility_lib/semver_checker.py b/compatibility_lib/compatibility_lib/semver_checker.py index 1e8e9714..2e0fb6db 100644 --- a/compatibility_lib/compatibility_lib/semver_checker.py +++ b/compatibility_lib/compatibility_lib/semver_checker.py @@ -18,7 +18,6 @@ # TODO: This needs more sophisticated logic -# - needs to look at args def check(old_dir, new_dir): """checks for semver breakage for two local directories it looks at all the attributes found by get_package_info @@ -30,7 +29,7 @@ def check(old_dir, new_dir): new_dir: directory containing new files Returns: - False if changes breaks semver, True if semver is preserved + a list of error strings describing semver breakages """ old_pkg_info = crawler.get_package_info(old_dir) new_pkg_info = crawler.get_package_info(new_dir) diff --git a/compatibility_lib/compatibility_lib/test_semver_checker.py b/compatibility_lib/compatibility_lib/test_semver_checker.py index 141dbee6..9f70e958 100644 --- a/compatibility_lib/compatibility_lib/test_semver_checker.py +++ b/compatibility_lib/compatibility_lib/test_semver_checker.py @@ -74,3 +74,10 @@ def test_semver_check_on_removed_args(self): res = check(old_dir, new_dir) expected = ['args do not match; expecting: "self, x", got: "self"'] self.assertEqual(expected, res) + + def test_sember_check_on_added_optional_args(self): + old_dir = os.path.join(TEST_DIR, 'optional_args/0.1.0') + new_dir = os.path.join(TEST_DIR, 'optional_args/0.2.0') + + res = check(old_dir, new_dir) + self.assertEqual([], res) diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/0.1.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/0.1.0/main.py new file mode 100644 index 00000000..b6cc03c2 --- /dev/null +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/0.1.0/main.py @@ -0,0 +1,23 @@ +# Copyright 2018 Google LLC +# +# 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. + + +class Foo(object): + + def __init__(self, x): + pass + + +def bar(a, b, c=1): + pass diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/0.2.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/0.2.0/main.py new file mode 100644 index 00000000..297cb5a4 --- /dev/null +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/0.2.0/main.py @@ -0,0 +1,23 @@ +# Copyright 2018 Google LLC +# +# 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. + + +class Foo(object): + + def __init__(self, x, y=None): + pass + + +def bar(a, b, c=None, d=True): + pass From e72032e9431e7677d3a3bfbedeef22ebe9f84234 Mon Sep 17 00:00:00 2001 From: Lily Li Date: Wed, 12 Dec 2018 15:09:57 -0800 Subject: [PATCH 2/8] fix typo in test name --- compatibility_lib/compatibility_lib/test_semver_checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compatibility_lib/compatibility_lib/test_semver_checker.py b/compatibility_lib/compatibility_lib/test_semver_checker.py index 9f70e958..1d752273 100644 --- a/compatibility_lib/compatibility_lib/test_semver_checker.py +++ b/compatibility_lib/compatibility_lib/test_semver_checker.py @@ -75,7 +75,7 @@ def test_semver_check_on_removed_args(self): expected = ['args do not match; expecting: "self, x", got: "self"'] self.assertEqual(expected, res) - def test_sember_check_on_added_optional_args(self): + def test_semver_check_on_added_optional_args(self): old_dir = os.path.join(TEST_DIR, 'optional_args/0.1.0') new_dir = os.path.join(TEST_DIR, 'optional_args/0.2.0') From 96475af7e82bf27d1dcb0ad20aa340b8fa62bc05 Mon Sep 17 00:00:00 2001 From: Lily Li Date: Mon, 17 Dec 2018 16:17:05 -0800 Subject: [PATCH 3/8] improve default arg detection - neither regular nor optional args cannot be removed - args can be added as optional args - arg order must be preserved - default values must be preserved - required args can be made optional - logic currently assumes *args and **kwargs are the last two args - added more test cases --- .../package_crawler_static.py | 69 +++++++++++--- .../compatibility_lib/semver_checker.py | 94 ++++++++++++++++--- .../compatibility_lib/test_semver_checker.py | 61 ++++++++++-- .../optional_args/appended/0.1.0/main.py | 27 ++++++ .../optional_args/appended/0.2.0/main.py | 28 ++++++ .../optional_args/appended/0.3.0/main.py | 30 ++++++ .../optional_args/converted/0.1.0/main.py | 23 +++++ .../optional_args/converted/0.2.0/main.py | 23 +++++ .../optional_args/inserted/0.1.0/main.py | 25 +++++ .../optional_args/inserted/0.2.0/main.py | 27 ++++++ .../optional_args/modified/0.1.0/main.py | 27 ++++++ .../optional_args/modified/0.2.0/main.py | 27 ++++++ .../{0.2.0 => removed/0.1.0}/main.py | 2 +- .../{0.1.0 => removed/0.2.0}/main.py | 0 14 files changed, 431 insertions(+), 32 deletions(-) create mode 100644 compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.1.0/main.py create mode 100644 compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.2.0/main.py create mode 100644 compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.3.0/main.py create mode 100644 compatibility_lib/compatibility_lib/testpkgs/optional_args/converted/0.1.0/main.py create mode 100644 compatibility_lib/compatibility_lib/testpkgs/optional_args/converted/0.2.0/main.py create mode 100644 compatibility_lib/compatibility_lib/testpkgs/optional_args/inserted/0.1.0/main.py create mode 100644 compatibility_lib/compatibility_lib/testpkgs/optional_args/inserted/0.2.0/main.py create mode 100644 compatibility_lib/compatibility_lib/testpkgs/optional_args/modified/0.1.0/main.py create mode 100644 compatibility_lib/compatibility_lib/testpkgs/optional_args/modified/0.2.0/main.py rename compatibility_lib/compatibility_lib/testpkgs/optional_args/{0.2.0 => removed/0.1.0}/main.py (95%) rename compatibility_lib/compatibility_lib/testpkgs/optional_args/{0.1.0 => removed/0.2.0}/main.py (100%) diff --git a/compatibility_lib/compatibility_lib/package_crawler_static.py b/compatibility_lib/compatibility_lib/package_crawler_static.py index b7aa574e..1fba8b3c 100644 --- a/compatibility_lib/compatibility_lib/package_crawler_static.py +++ b/compatibility_lib/compatibility_lib/package_crawler_static.py @@ -63,9 +63,14 @@ def get_package_info(root_dir): module_name: { 'classes': { class_name: { - 'args': [arg1, arg2, ...], + 'args': { + 'single_args': [arg1, arg2, ...], + 'defaults': {arg1: value1, ...}, + 'vararg': argv, + 'kwarg': kwargs, + }, 'functions': { - function_name: {'args': [...]}, + function_name: {'args': {...}}, } }, class_name: {...}, @@ -118,9 +123,14 @@ def get_module_info(node): { 'classes': { class_name: { - 'args': [arg1, arg2, ...], + 'args': { + 'single_args': [arg1, arg2, ...], + 'defaults': {arg1: value1, ...}, + 'vararg': argv, + 'kwarg': kwargs, + }, 'functions': { - function_name: {'args': [...]}, + function_name: {'args': {...}}, } }, class_name: {...}, @@ -219,13 +229,44 @@ def _get_function_info(functions): def _get_args(node): - """returns a list of non-default args, ignoring args with defaults""" - args = [] - num_args = len(node.args) - len(node.defaults) - for i in range(num_args): - arg = node.args[i] - if isinstance(arg, ast.arg): - args.append(arg.arg) - elif isinstance(arg, ast.Name): - args.append(arg.id) - return args + """returns a dict mapping arg type to arg names""" + args, default_args, vararg, kwarg = [], {}, None, None + num_required_args = len(node.args) - len(node.defaults) + + for i, argnode in enumerate(node.args): + arg = None + if isinstance(argnode, ast.arg): + arg = argnode.arg + elif isinstance(argnode, ast.Name): + arg = argnode.id + args.append(arg) + + if i >= num_required_args: + valnode = node.defaults[i-len(node.args)] + if isinstance(valnode, ast.NameConstant): + # bools, nonetype + value = valnode.value + elif isinstance(valnode, ast.Num): + # ints + value = valnode.n + elif isinstance(valnode, ast.Str): + value = valnode.s + elif isinstance(valnode, ast.List): + value = valnode.elts + else: + print(valnode) + from pdb import set_trace; set_trace() + default_args[arg] = value + + if node.vararg: + vararg = node.vararg.arg + if node.kwarg: + kwarg = node.kwarg.arg + + res = {} + res['single_args'] = args + res['defaults'] = default_args + res['vararg'] = vararg + res['kwarg'] = kwarg + + return res diff --git a/compatibility_lib/compatibility_lib/semver_checker.py b/compatibility_lib/compatibility_lib/semver_checker.py index 2e0fb6db..6d1cd27f 100644 --- a/compatibility_lib/compatibility_lib/semver_checker.py +++ b/compatibility_lib/compatibility_lib/semver_checker.py @@ -14,9 +14,19 @@ """checks two packages for semver breakage""" +import enum from compatibility_lib import package_crawler_static as crawler +class _Error(enum.Enum): + MISSING = '%s: missing arg "%s"' + NUM_TOTAL = '%s: expected %s args, got %s' + NUM_REQUIRED = '%s: expected %s required args, got %s' + BAD_ARG = '%s: bad arg name; expected "%s", got "%s"' + BAD_VALUE = ('%s: default value was not preserved; ' + 'expecting "%s=%s", got "%s=%s"') + + # TODO: This needs more sophisticated logic def check(old_dir, new_dir): """checks for semver breakage for two local directories @@ -29,29 +39,91 @@ def check(old_dir, new_dir): new_dir: directory containing new files Returns: - a list of error strings describing semver breakages + A list of error strings describing semver breakages """ + qn = () old_pkg_info = crawler.get_package_info(old_dir) new_pkg_info = crawler.get_package_info(new_dir) - unseen = [(old_pkg_info, new_pkg_info)] + unseen = [(qn, old_pkg_info, new_pkg_info)] errors = [] - missing = 'missing attribute "%s" from new version' - bad_args = 'args do not match; expecting: "%s", got: "%s"' i = 0 while i < len(unseen): - old, new = unseen[i] + qn, old, new = unseen[i] for key in old.keys(): if new.get(key) is None: - errors.append(missing % key) + msg = _Error.MISSING.value + errors.append(msg % (_get_qn_str(qn), key)) continue if key != 'args': - unseen.append((old[key], new[key])) - elif old[key] != new[key]: - old_args = ', '.join(old[key]) - new_args = ', '.join(new[key]) - errors.append(bad_args % (old_args, new_args)) + new_qn = qn + (key,) + unseen.append((new_qn, old[key], new[key])) + else: + # TODO: better error messages + new_errors = _check_args(qn, old[key], new[key]) + errors.extend(new_errors) i += 1 return errors + + +def _get_qn_str(qn_list): + """returns the qualified name string""" + clean_qn = [name for i, name in enumerate(qn_list) if i % 2 == 1] + res = '.'.join(clean_qn) + return res + + +def _check_args(qn_list, old_args, new_args): + errors = [] + qn = _get_qn_str(qn_list) + missing = _Error.MISSING.value.replace('%s', qn, 1) + num_total = _Error.NUM_TOTAL.value.replace('%s', qn, 1) + num_required = _Error.NUM_REQUIRED.value.replace('%s', qn, 1) + bad_arg = _Error.BAD_ARG.value.replace('%s', qn, 1) + bad_value = _Error.BAD_VALUE.value.replace('%s', qn, 1) + + # check old args against new args + old_single_args = old_args['single_args'] + new_single_args = new_args['single_args'] + if len(old_single_args) > len(new_single_args): + res = [num_total % (len(old_single_args), len(new_single_args))] + return res + for i, _ in enumerate(old_single_args): + if old_single_args[i] != new_single_args[i]: + res = [bad_arg % (old_single_args[i], new_single_args[i])] + return res + + old_defaults = old_args['defaults'] + new_defaults = new_args['defaults'] + num_old_req_args = len(old_single_args) - len(old_defaults) + num_new_req_args = len(new_single_args) - len(new_defaults) + + # check required args match up + for i in range(max(num_old_req_args, num_new_req_args)): + if i == len(old_single_args) or i == len(new_single_args): + msg = num_required % (num_old_req_args, num_new_req_args) + errors.append(msg) + break + + # if old_single_args[i] != new_single_args[i]: + # msg = bad_arg % (old_single_args[i], new_single_args[i]) + # errors.append(msg) + # break + + # check default arg values + for key in old_defaults: + if key not in new_defaults: + errors.append(missing % key) + if old_defaults[key] != new_defaults[key]: + errors.append(bad_value % (key, old_defaults[key], + key, new_defaults[key])) + + # check vararg and kwarg + if old_args['vararg'] and old_args['vararg'] != new_args['vararg']: + errors.append(missing % old_args['vararg']) + if old_args['kwarg'] and old_args['kwarg'] != new_args['kwarg']: + errors.append(missing % old_args['kwarg']) + + return errors diff --git a/compatibility_lib/compatibility_lib/test_semver_checker.py b/compatibility_lib/compatibility_lib/test_semver_checker.py index 1d752273..0d4ccadd 100644 --- a/compatibility_lib/compatibility_lib/test_semver_checker.py +++ b/compatibility_lib/compatibility_lib/test_semver_checker.py @@ -33,7 +33,12 @@ def test_get_package_info(self): 'classes': {}, 'functions': { 'hello': { - 'args': [] + 'args': { + 'single_args': [], + 'defaults': {}, + 'vararg': None, + 'kwarg': None + } } } }, @@ -56,7 +61,7 @@ def test_semver_check_on_removed_func(self): new_dir = os.path.join(TEST_DIR, 'removed_func/0.2.0') res = check(old_dir, new_dir) - expected = ['missing attribute "bar" from new version'] + expected = ['main: missing arg "bar"'] self.assertEqual(expected, res) def test_semver_check_on_added_args(self): @@ -64,7 +69,7 @@ def test_semver_check_on_added_args(self): new_dir = os.path.join(TEST_DIR, 'added_args/0.2.0') res = check(old_dir, new_dir) - expected = ['args do not match; expecting: "self, x", got: "self, x, y"'] + expected = ['main.Foo: expected 2 required args, got 3'] self.assertEqual(expected, res) def test_semver_check_on_removed_args(self): @@ -72,12 +77,56 @@ def test_semver_check_on_removed_args(self): new_dir = os.path.join(TEST_DIR, 'removed_args/0.2.0') res = check(old_dir, new_dir) - expected = ['args do not match; expecting: "self, x", got: "self"'] + expected = ['main.Foo: expected 2 args, got 1'] self.assertEqual(expected, res) def test_semver_check_on_added_optional_args(self): - old_dir = os.path.join(TEST_DIR, 'optional_args/0.1.0') - new_dir = os.path.join(TEST_DIR, 'optional_args/0.2.0') + ver1 = os.path.join(TEST_DIR, 'optional_args/appended/0.1.0') + ver2 = os.path.join(TEST_DIR, 'optional_args/appended/0.2.0') + ver3 = os.path.join(TEST_DIR, 'optional_args/appended/0.3.0') + + res12 = check(ver1, ver2) + res23 = check(ver2, ver3) + res13 = check(ver1, ver3) + + self.assertEqual([], res12) + self.assertEqual([], res23) + self.assertEqual([], res13) + + def test_semver_check_on_removed_optional_args(self): + old_dir = os.path.join(TEST_DIR, 'optional_args/removed/0.1.0') + new_dir = os.path.join(TEST_DIR, 'optional_args/removed/0.2.0') + + res = check(old_dir, new_dir) + expected = ['main.Foo: expected 3 args, got 2', + 'main.bar: missing arg "d"'] + self.assertEqual(expected, res) + + def test_semver_check_on_inserted_optional_args(self): + old_dir = os.path.join(TEST_DIR, 'optional_args/inserted/0.1.0') + new_dir = os.path.join(TEST_DIR, 'optional_args/inserted/0.2.0') + + res = check(old_dir, new_dir) + expected = ['main.Foo: bad arg name; expected "y", got "z"'] + self.assertEqual(expected, res) + + def test_semver_check_on_modified_optional_args(self): + old_dir = os.path.join(TEST_DIR, 'optional_args/modified/0.1.0') + new_dir = os.path.join(TEST_DIR, 'optional_args/modified/0.2.0') + + res = check(old_dir, new_dir) + expected = [('main.Foo: default value was not preserved; ' + 'expecting "y=None", got "y=True"'), + ('main.bar: default value was not preserved; ' + 'expecting "c=1", got "c=2"'), + ('main.bar: default value was not preserved; ' + 'expecting "d=2", got "d=1"'), + 'main.baz: bad arg name; expected "name", got "first_name"'] + self.assertEqual(expected, res) + + def test_semver_check_on_converted_optional_args(self): + old_dir = os.path.join(TEST_DIR, 'optional_args/converted/0.1.0') + new_dir = os.path.join(TEST_DIR, 'optional_args/converted/0.2.0') res = check(old_dir, new_dir) self.assertEqual([], res) diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.1.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.1.0/main.py new file mode 100644 index 00000000..f88e9d12 --- /dev/null +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.1.0/main.py @@ -0,0 +1,27 @@ +# Copyright 2018 Google LLC +# +# 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. + + +class Foo(object): + + def __init__(self, x): + pass + + +def bar(a, b, c=1): + return (a + b) * c + + +if __name__ == '__main__': + print(bar(1, 2)) diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.2.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.2.0/main.py new file mode 100644 index 00000000..8058844c --- /dev/null +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.2.0/main.py @@ -0,0 +1,28 @@ +# Copyright 2018 Google LLC +# +# 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. + + +class Foo(object): + + def __init__(self, x, y=None): + pass + + +def bar(a, b, c=1, d=True): + if d: + return (a + b) * c + return 0 + +if __name__ == '__main__': + print(bar(1, 2)) diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.3.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.3.0/main.py new file mode 100644 index 00000000..9fce7c2b --- /dev/null +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.3.0/main.py @@ -0,0 +1,30 @@ +# Copyright 2018 Google LLC +# +# 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. + + +class Foo(object): + + def __init__(self, x, y=None, **z): + pass + + +def bar(a, b, c=1, d=True, *e): + if args: + return sum(args) + if d: + return (a + b) * c + return 0 + +if __name__ == '__main__': + print(bar(1, 2)) diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/converted/0.1.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/converted/0.1.0/main.py new file mode 100644 index 00000000..0f290dc3 --- /dev/null +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/converted/0.1.0/main.py @@ -0,0 +1,23 @@ +# Copyright 2018 Google LLC +# +# 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. + + +class Foo(object): + + def __init__(self, x, y): + pass + + +def bar(a, b, c): + pass diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/converted/0.2.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/converted/0.2.0/main.py new file mode 100644 index 00000000..138d6ded --- /dev/null +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/converted/0.2.0/main.py @@ -0,0 +1,23 @@ +# Copyright 2018 Google LLC +# +# 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. + + +class Foo(object): + + def __init__(self, x, y=[]): + pass + + +def bar(a=1, b=2, c=3, d=4, e=5): + pass diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/inserted/0.1.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/inserted/0.1.0/main.py new file mode 100644 index 00000000..e65eb3f0 --- /dev/null +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/inserted/0.1.0/main.py @@ -0,0 +1,25 @@ +# Copyright 2018 Google LLC +# +# 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. + + +class Foo(object): + + def __init__(self, x, y=None): + pass + + +def bar(a, b, c=1, d=True): + if d: + return (a + b) * c + return 0 diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/inserted/0.2.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/inserted/0.2.0/main.py new file mode 100644 index 00000000..6a4b07d5 --- /dev/null +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/inserted/0.2.0/main.py @@ -0,0 +1,27 @@ +# Copyright 2018 Google LLC +# +# 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. + + +class Foo(object): + + def __init__(self, x, z=1, y=None): + pass + + +def bar(a, b, c=1, d=True, *args): + if args: + return sum(args) + if d: + return (a + b) * c + return 0 diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/modified/0.1.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/modified/0.1.0/main.py new file mode 100644 index 00000000..95adb3f8 --- /dev/null +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/modified/0.1.0/main.py @@ -0,0 +1,27 @@ +# Copyright 2018 Google LLC +# +# 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. + + +class Foo(object): + + def __init__(self, x, y=None): + pass + + +def bar(a, b, c=1, d=2): + pass + + +def baz(name='bob', age=18): + pass diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/modified/0.2.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/modified/0.2.0/main.py new file mode 100644 index 00000000..d3433381 --- /dev/null +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/modified/0.2.0/main.py @@ -0,0 +1,27 @@ +# Copyright 2018 Google LLC +# +# 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. + + +class Foo(object): + + def __init__(self, x, y=True): + pass + + +def bar(a, b, c=2, d=1): + pass + + +def baz(first_name='bob', age=18): + pass diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/0.2.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/removed/0.1.0/main.py similarity index 95% rename from compatibility_lib/compatibility_lib/testpkgs/optional_args/0.2.0/main.py rename to compatibility_lib/compatibility_lib/testpkgs/optional_args/removed/0.1.0/main.py index 297cb5a4..c291e05d 100644 --- a/compatibility_lib/compatibility_lib/testpkgs/optional_args/0.2.0/main.py +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/removed/0.1.0/main.py @@ -19,5 +19,5 @@ def __init__(self, x, y=None): pass -def bar(a, b, c=None, d=True): +def bar(a, b, c=1, *d): pass diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/0.1.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/removed/0.2.0/main.py similarity index 100% rename from compatibility_lib/compatibility_lib/testpkgs/optional_args/0.1.0/main.py rename to compatibility_lib/compatibility_lib/testpkgs/optional_args/removed/0.2.0/main.py From 1764235810a1e40df7f01ae649d8b6324327f87b Mon Sep 17 00:00:00 2001 From: Lily Li Date: Mon, 17 Dec 2018 16:48:27 -0800 Subject: [PATCH 4/8] clean up package_crawler --- .../compatibility_lib/package_crawler_static.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/compatibility_lib/compatibility_lib/package_crawler_static.py b/compatibility_lib/compatibility_lib/package_crawler_static.py index 1fba8b3c..8ac720d8 100644 --- a/compatibility_lib/compatibility_lib/package_crawler_static.py +++ b/compatibility_lib/compatibility_lib/package_crawler_static.py @@ -244,18 +244,21 @@ def _get_args(node): if i >= num_required_args: valnode = node.defaults[i-len(node.args)] if isinstance(valnode, ast.NameConstant): - # bools, nonetype value = valnode.value elif isinstance(valnode, ast.Num): - # ints value = valnode.n elif isinstance(valnode, ast.Str): value = valnode.s - elif isinstance(valnode, ast.List): + elif isinstance(valnode, (ast.List, ast.Tuple)): value = valnode.elts + elif isinstance(valnode, ast.Dict): + value = {} + for i, key in enumerate(valnode.keys): + value[key] = valnode.values[i] else: - print(valnode) - from pdb import set_trace; set_trace() + # TODO: provide better error messaging + raise Exception('%s:%s: unsupported default arg type' % + (valnode.lineno, valnode.col_offset)) default_args[arg] = value if node.vararg: From f2ea720fff853084aeadb2ad1f5550dabffab81f Mon Sep 17 00:00:00 2001 From: Lily Li Date: Tue, 18 Dec 2018 11:13:52 -0800 Subject: [PATCH 5/8] fix test --- compatibility_lib/compatibility_lib/test_semver_checker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compatibility_lib/compatibility_lib/test_semver_checker.py b/compatibility_lib/compatibility_lib/test_semver_checker.py index 0d4ccadd..f7de0d84 100644 --- a/compatibility_lib/compatibility_lib/test_semver_checker.py +++ b/compatibility_lib/compatibility_lib/test_semver_checker.py @@ -122,7 +122,8 @@ def test_semver_check_on_modified_optional_args(self): ('main.bar: default value was not preserved; ' 'expecting "d=2", got "d=1"'), 'main.baz: bad arg name; expected "name", got "first_name"'] - self.assertEqual(expected, res) + for errmsg in res: + self.assertTrue(errmsg in expected) def test_semver_check_on_converted_optional_args(self): old_dir = os.path.join(TEST_DIR, 'optional_args/converted/0.1.0') From 31b95c7cceea9022b362859057b873e8c9f3223b Mon Sep 17 00:00:00 2001 From: Lily Li Date: Tue, 18 Dec 2018 16:18:17 -0800 Subject: [PATCH 6/8] update default value for "args" --- .../compatibility_lib/package_crawler_static.py | 3 ++- .../compatibility_lib/test_semver_checker.py | 9 ++++++--- .../compatibility_lib/testpkgs/added_args/0.1.0/main.py | 4 ++++ .../compatibility_lib/testpkgs/added_args/0.2.0/main.py | 4 ++++ .../testpkgs/optional_args/appended/0.1.0/main.py | 4 ++++ .../testpkgs/optional_args/appended/0.2.0/main.py | 5 +++++ .../testpkgs/optional_args/appended/0.3.0/main.py | 5 +++++ .../testpkgs/optional_args/removed/0.1.0/main.py | 4 ++++ .../testpkgs/optional_args/removed/0.2.0/main.py | 4 ++++ .../testpkgs/removed_args/0.1.0/main.py | 4 ++++ .../testpkgs/removed_args/0.2.0/main.py | 4 ++++ 11 files changed, 46 insertions(+), 4 deletions(-) diff --git a/compatibility_lib/compatibility_lib/package_crawler_static.py b/compatibility_lib/compatibility_lib/package_crawler_static.py index 8ac720d8..a0ff8ccf 100644 --- a/compatibility_lib/compatibility_lib/package_crawler_static.py +++ b/compatibility_lib/compatibility_lib/package_crawler_static.py @@ -161,7 +161,8 @@ def _get_class_info(classes): # assumption is that bases are user-defined within the same module init_func, subclasses, functions = _get_class_attrs(node, classes) - args = [] + args = {'single_args': [], 'defaults': {}, + 'vararg': None, 'kwarg': None} if init_func is not None: args = _get_args(init_func.args) diff --git a/compatibility_lib/compatibility_lib/test_semver_checker.py b/compatibility_lib/compatibility_lib/test_semver_checker.py index f7de0d84..fd14d09d 100644 --- a/compatibility_lib/compatibility_lib/test_semver_checker.py +++ b/compatibility_lib/compatibility_lib/test_semver_checker.py @@ -69,7 +69,8 @@ def test_semver_check_on_added_args(self): new_dir = os.path.join(TEST_DIR, 'added_args/0.2.0') res = check(old_dir, new_dir) - expected = ['main.Foo: expected 2 required args, got 3'] + expected = ['main.Foo: expected 2 required args, got 3', + 'main.bar: expected 0 required args, got 1'] self.assertEqual(expected, res) def test_semver_check_on_removed_args(self): @@ -77,7 +78,8 @@ def test_semver_check_on_removed_args(self): new_dir = os.path.join(TEST_DIR, 'removed_args/0.2.0') res = check(old_dir, new_dir) - expected = ['main.Foo: expected 2 args, got 1'] + expected = ['main.Foo: expected 2 args, got 1', + 'main.bar: expected 1 args, got 0'] self.assertEqual(expected, res) def test_semver_check_on_added_optional_args(self): @@ -99,7 +101,8 @@ def test_semver_check_on_removed_optional_args(self): res = check(old_dir, new_dir) expected = ['main.Foo: expected 3 args, got 2', - 'main.bar: missing arg "d"'] + 'main.bar: missing arg "d"', + 'main.baz: expected 1 args, got 0'] self.assertEqual(expected, res) def test_semver_check_on_inserted_optional_args(self): diff --git a/compatibility_lib/compatibility_lib/testpkgs/added_args/0.1.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/added_args/0.1.0/main.py index b31946d3..4571b641 100644 --- a/compatibility_lib/compatibility_lib/testpkgs/added_args/0.1.0/main.py +++ b/compatibility_lib/compatibility_lib/testpkgs/added_args/0.1.0/main.py @@ -17,3 +17,7 @@ class Foo(object): def __init__(self, x): pass + + +def bar(): + pass diff --git a/compatibility_lib/compatibility_lib/testpkgs/added_args/0.2.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/added_args/0.2.0/main.py index 117f193e..d7850189 100644 --- a/compatibility_lib/compatibility_lib/testpkgs/added_args/0.2.0/main.py +++ b/compatibility_lib/compatibility_lib/testpkgs/added_args/0.2.0/main.py @@ -17,3 +17,7 @@ class Foo(object): def __init__(self, x, y): pass + + +def bar(a): + pass diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.1.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.1.0/main.py index f88e9d12..dd504c13 100644 --- a/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.1.0/main.py +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.1.0/main.py @@ -23,5 +23,9 @@ def bar(a, b, c=1): return (a + b) * c +def baz(): + pass + + if __name__ == '__main__': print(bar(1, 2)) diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.2.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.2.0/main.py index 8058844c..4612acbb 100644 --- a/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.2.0/main.py +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.2.0/main.py @@ -24,5 +24,10 @@ def bar(a, b, c=1, d=True): return (a + b) * c return 0 + +def baz(baz='baz'): + pass + + if __name__ == '__main__': print(bar(1, 2)) diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.3.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.3.0/main.py index 9fce7c2b..f0ec8a1a 100644 --- a/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.3.0/main.py +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.3.0/main.py @@ -26,5 +26,10 @@ def bar(a, b, c=1, d=True, *e): return (a + b) * c return 0 + +def baz(baz='baz'): + pass + + if __name__ == '__main__': print(bar(1, 2)) diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/removed/0.1.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/removed/0.1.0/main.py index c291e05d..c0487520 100644 --- a/compatibility_lib/compatibility_lib/testpkgs/optional_args/removed/0.1.0/main.py +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/removed/0.1.0/main.py @@ -21,3 +21,7 @@ def __init__(self, x, y=None): def bar(a, b, c=1, *d): pass + + +def baz(baz='baz'): + pass diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/removed/0.2.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/removed/0.2.0/main.py index b6cc03c2..e86e1808 100644 --- a/compatibility_lib/compatibility_lib/testpkgs/optional_args/removed/0.2.0/main.py +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/removed/0.2.0/main.py @@ -21,3 +21,7 @@ def __init__(self, x): def bar(a, b, c=1): pass + + +def baz(): + pass diff --git a/compatibility_lib/compatibility_lib/testpkgs/removed_args/0.1.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/removed_args/0.1.0/main.py index b31946d3..37a0165e 100644 --- a/compatibility_lib/compatibility_lib/testpkgs/removed_args/0.1.0/main.py +++ b/compatibility_lib/compatibility_lib/testpkgs/removed_args/0.1.0/main.py @@ -17,3 +17,7 @@ class Foo(object): def __init__(self, x): pass + + +def bar(a): + pass diff --git a/compatibility_lib/compatibility_lib/testpkgs/removed_args/0.2.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/removed_args/0.2.0/main.py index f494c254..2ea8fbd4 100644 --- a/compatibility_lib/compatibility_lib/testpkgs/removed_args/0.2.0/main.py +++ b/compatibility_lib/compatibility_lib/testpkgs/removed_args/0.2.0/main.py @@ -17,3 +17,7 @@ class Foo(object): def __init__(self): pass + + +def bar(): + pass From 50765e6d0a1b531067b0e41e5f0f2d18ebf2e1d0 Mon Sep 17 00:00:00 2001 From: Lily Li Date: Wed, 19 Dec 2018 14:45:03 -0800 Subject: [PATCH 7/8] fix tests --- .../compatibility_lib/test_semver_checker.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/compatibility_lib/compatibility_lib/test_semver_checker.py b/compatibility_lib/compatibility_lib/test_semver_checker.py index fd14d09d..a9fbcec4 100644 --- a/compatibility_lib/compatibility_lib/test_semver_checker.py +++ b/compatibility_lib/compatibility_lib/test_semver_checker.py @@ -71,7 +71,8 @@ def test_semver_check_on_added_args(self): res = check(old_dir, new_dir) expected = ['main.Foo: expected 2 required args, got 3', 'main.bar: expected 0 required args, got 1'] - self.assertEqual(expected, res) + for errmsg in res: + self.assertTrue(errmsg in expected) def test_semver_check_on_removed_args(self): old_dir = os.path.join(TEST_DIR, 'removed_args/0.1.0') @@ -80,7 +81,8 @@ def test_semver_check_on_removed_args(self): res = check(old_dir, new_dir) expected = ['main.Foo: expected 2 args, got 1', 'main.bar: expected 1 args, got 0'] - self.assertEqual(expected, res) + for errmsg in res: + self.assertTrue(errmsg in expected) def test_semver_check_on_added_optional_args(self): ver1 = os.path.join(TEST_DIR, 'optional_args/appended/0.1.0') @@ -103,7 +105,8 @@ def test_semver_check_on_removed_optional_args(self): expected = ['main.Foo: expected 3 args, got 2', 'main.bar: missing arg "d"', 'main.baz: expected 1 args, got 0'] - self.assertEqual(expected, res) + for errmsg in res: + self.assertTrue(errmsg in expected) def test_semver_check_on_inserted_optional_args(self): old_dir = os.path.join(TEST_DIR, 'optional_args/inserted/0.1.0') From 51d5bf240ce11e397284ad559025e3203a1da17a Mon Sep 17 00:00:00 2001 From: Lily Li Date: Wed, 19 Dec 2018 15:11:19 -0800 Subject: [PATCH 8/8] fix lint --- .../testpkgs/optional_args/appended/0.1.0/main.py | 2 +- .../testpkgs/optional_args/appended/0.3.0/main.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.1.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.1.0/main.py index dd504c13..7487786f 100644 --- a/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.1.0/main.py +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.1.0/main.py @@ -20,7 +20,7 @@ def __init__(self, x): def bar(a, b, c=1): - return (a + b) * c + return (a + b) * c def baz(): diff --git a/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.3.0/main.py b/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.3.0/main.py index f0ec8a1a..fdfbd07e 100644 --- a/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.3.0/main.py +++ b/compatibility_lib/compatibility_lib/testpkgs/optional_args/appended/0.3.0/main.py @@ -20,8 +20,8 @@ def __init__(self, x, y=None, **z): def bar(a, b, c=1, d=True, *e): - if args: - return sum(args) + if e: + return sum(e) if d: return (a + b) * c return 0