From 5369eea710919f2d7d60a1efc1c446061474b874 Mon Sep 17 00:00:00 2001 From: Peter Dragun Date: Thu, 3 Aug 2023 13:18:38 +0200 Subject: [PATCH] fix(tools): check_python_version accept dev releases as requirement --- tools/check_python_dependencies.py | 2 +- .../test_idf_tools_python_env.py | 223 +++++++++++++++++- 2 files changed, 223 insertions(+), 2 deletions(-) diff --git a/tools/check_python_dependencies.py b/tools/check_python_dependencies.py index 145a3a5d79..f342eaf680 100755 --- a/tools/check_python_dependencies.py +++ b/tools/check_python_dependencies.py @@ -84,7 +84,7 @@ if __name__ == '__main__': def version_check(requirement: Requirement) -> None: # compare installed version with required version = Version(get_version(requirement.name)) - if version.base_version not in requirement.specifier: + if not requirement.specifier.contains(version, prereleases=True): not_satisfied.append(f"Requirement '{requirement}' was not met. Installed version: {version}") # evaluate markers and check versions of direct requirements diff --git a/tools/test_idf_tools/test_idf_tools_python_env.py b/tools/test_idf_tools/test_idf_tools_python_env.py index 2ea7e301d6..91557fb16c 100644 --- a/tools/test_idf_tools/test_idf_tools_python_env.py +++ b/tools/test_idf_tools/test_idf_tools_python_env.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 +import inspect import os import shutil import subprocess @@ -23,8 +24,133 @@ REQ_CORE = '- {}'.format(os.path.join(IDF_PATH, 'tools', 'requirements', 'requir REQ_GDBGUI = '- {}'.format(os.path.join(IDF_PATH, 'tools', 'requirements', 'requirements.gdbgui.txt')) CONSTR = 'Constraint file: {}/espidf.constraints'.format(TOOLS_DIR) +# Set default global paths for idf_tools. If some test needs to +# use functions from idf_tools with custom paths, it should +# set it in setUp() and change them back to defaults in tearDown(). +idf_tools.global_idf_path = IDF_PATH +idf_tools.global_idf_tools_path = TOOLS_DIR -class TestPythonInstall(unittest.TestCase): + +class BasePythonInstall(unittest.TestCase): + def run_tool(self, cmd): # type: (List[str]) -> str + ret = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=300) + decoded_output = ret.stdout.decode('utf-8', 'ignore') + with open(os.path.join(IDF_PATH, 'tools', 'test_idf_tools', 'test_python_env_logs.txt'), 'a+') as w: + # stack() returns list of callers frame records. [1] represent caller of this function + w.write('============================= ' + inspect.stack()[1].function + ' =============================\n') + w.write(decoded_output) + return decoded_output + + def run_idf_tools(self, args): # type: (List[str]) -> str + cmd = [sys.executable, '../idf_tools.py'] + args + return self.run_tool(cmd) + + def run_in_venv(self, args): # type: (List[str]) -> str + _, _, python_venv, _ = idf_tools.get_python_env_path() + cmd = [python_venv] + args + return self.run_tool(cmd) + + def dump_package(self, whl, name): # type: (bytes, str) -> str + tmpdir = tempfile.mkdtemp() + foopackage_fn = os.path.join(tmpdir, name) + with open(foopackage_fn, 'wb') as fd: + fd.write(whl) + + self.addCleanup(shutil.rmtree, tmpdir) + return foopackage_fn + + def dump_foopackage(self): # type: () -> str + # Wheel for foopackage-0.99-py3-none-any.whl + # This is dummy package for testing purposes created with + # python -m build --wheel for the following package + ''' + ├── foopackage + │   └── __init__.py + └── setup.py + + setup.py + from setuptools import setup + + setup( + name="foopackage", + version="0.99", + ) + + __init__.py + if __name__ == '__main__': + return + ''' + + whl = (b'PK\x03\x04\x14\x00\x00\x00\x08\x00\x07fqVz|E\t&\x00\x00\x00&\x00\x00\x00\x16\x00\x00\x00' + b'foopackage/__init__.py\xcbLS\x88\x8f\xcfK\xccM\x8d\x8fW\xb0\xb5UP\x8f\x8f\xcfM\xcc\xcc\x8b\x8fW' + b'\xb7\xe2R\x00\x82\xa2\xd4\x92\xd2\xa2<.\x00PK\x03\x04\x14\x00\x00\x00\x08\x00%fqV\x8d\x90\x81\x05' + b'1\x00\x00\x006\x00\x00\x00"\x00\x00\x00foopackage-0.99.dist-info/METADATA\xf3M-ILI,I\xd4\rK-*\xce' + b'\xcc\xcf\xb3R0\xd23\xe4\xf2K\xccM\xb5RH\xcb\xcf/HL\xceNLO\xe5\x82\xcb\x1a\xe8YZrq\x01\x00PK\x03\x04' + b'\x14\x00\x00\x00\x08\x00%fqVI\xa2!\xcb\\\x00\x00\x00\\\x00\x00\x00\x1f\x00\x00\x00foopackage-0.99' + b'.dist-info/WHEEL\x0b\xcfHM\xcd\xd1\rK-*\xce\xcc\xcf\xb3R0\xd43\xe0rO\xcdK-J,\xc9/\xb2RHJ\xc9,.\x89/' + b'\x07\xa9Q\xd00\xd031\xd03\xd0\xe4\n\xca\xcf/\xd1\xf5,\xd6\r(-J\xcd\xc9L\xb2R()*M\xe5\nIL\xb7R(\xa84' + b'\xd6\xcd\xcb\xcfK\xd5M\xcc\xab\xe4\xe2\x02\x00PK\x03\x04\x14\x00\x00\x00\x08\x00%fqVI*\x9e\xa7\r\x00' + b'\x00\x00\x0b\x00\x00\x00\'\x00\x00\x00foopackage-0.99.dist-info/top_level.txtK\xcb\xcf/HL\xceNLO\xe5' + b'\x02\x00PK\x03\x04\x14\x00\x00\x00\x08\x00%fqV&\xdc\x9b\x88\xfd\x00\x00\x00}\x01\x00\x00 \x00\x00\x00' + b'foopackage-0.99.dist-info/RECORD}\xcc;\x92\x820\x00\x00\xd0\xde\xb3\x04\xe4#\xbfb\x8b\xac\xb0\x0b,' + b'\xa8\x83\x02#M&\x08\x81\x80\x02c\x02\x82\xa7\xb7rK\xdf\x01\x1e\xe9\xfb\x01_Z\\\x95k\x84hG9B\xe2\xb0' + b'\x00VcE\xd3\xbf\xf4\xe6\xe1\t6a2\xc3\x16N\x06]1Bm\xb7\x17\xc2Z\xef\xaa\xed\xf6\x9c\xdaQ \xd0\xf6\xc6' + b':\xec\x00\xd5\\\x91\xffL\x90D\xcb\x12\x0b\xca\xb8@;\xd2\xafC\xe7\x04mx\x82\xef\xb8\xf2\xc6"\xd9\xdd' + b'\r\x18\xe4\xcd\xef=\xf7\n7\x9eg4?\xa7\x04V*gXI\xff\xcanD\xc1\xf1\xc0\x80\xb6\xf9\x10\xa7\xae\xe3\x04' + b'\xefuh/<;?\xe3\xe3\x06\x9e\x93N/|\xc1Puc\xefgt\xfaQJ3\x82V\x8e\xb2\xef\x86\x12\xd9\x04\x96\xf2a\xe5' + b'\xfd\x80\xae\xe5T^E>\xf3\xf7\x1eW\x122\xe4\x91\xfbi\x1f\xd6\xeem\x99\xd4\xec\x11Ju\x9d\'R\xc83R\x19>' + b'jbO:\xb8\x8b\td\xf9\xc3\x1e9\xdb}d\x03\xb0z\x01PK\x01\x02\x14\x03\x14\x00\x00\x00\x08\x00\x07fqVz|E\t' + b'&\x00\x00\x00&\x00\x00\x00\x16\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00' + b'foopackage/__init__.pyPK\x01\x02\x14\x03\x14\x00\x00\x00\x08\x00%fqV\x8d\x90\x81\x051\x00\x00\x006\x00' + b'\x00\x00"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81Z\x00\x00\x00foopackage-0.99.dist-info' + b'/METADATAPK\x01\x02\x14\x03\x14\x00\x00\x00\x08\x00%fqVI\xa2!\xcb\\\x00\x00\x00\\\x00\x00\x00\x1f\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xcb\x00\x00\x00foopackage-0.99.dist-info/WHEELPK\x01' + b'\x02\x14\x03\x14\x00\x00\x00\x08\x00%fqVI*\x9e\xa7\r\x00\x00\x00\x0b\x00\x00\x00\'\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\xa4\x81d\x01\x00\x00foopackage-0.99.dist-info/top_level.txtPK\x01\x02\x14\x03' + b'\x14\x00\x00\x00\x08\x00%fqV&\xdc\x9b\x88\xfd\x00\x00\x00}\x01\x00\x00 \x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\xb4\x81\xb6\x01\x00\x00foopackage-0.99.dist-info/RECORDPK\x05\x06\x00\x00\x00\x00\x05' + b'\x00\x05\x00\x84\x01\x00\x00\xf1\x02\x00\x00\x00\x00') + + return self.dump_package(whl, 'foopackage-0.99-py3-none-any.whl') + + def dump_foopackage_dev(self): # type: () -> str + # similar to dump_foopackage, but using dev release version + + whl = (b'PK\x03\x04\x14\x00\x00\x00\x08\x00\nl\x03W !Z\xfc%\x00\x00\x00%\x00\x00\x00\x16\x00\x00\x00' + b'foopackage/__init__.py\xcbLS\x88\x8f\xcfK\xccM\x8d\x8fW\xb0\xb5UP\x8f\x8f\xcfM\xcc\xcc\x8b\x8fW\xb7' + b'\xe2R\x00\x82\xa2\xd4\x92\xd2\xa2<\x00PK\x03\x04\x14\x00\x00\x00\x08\x00Jl\x03W\xb4wO\x876\x00\x00' + b'\x00;\x00\x00\x00\'\x00\x00\x00foopackage-0.99.dev0.dist-info/METADATA\xf3M-ILI,I\xd4\rK-*\xce\xcc' + b'\xcf\xb3R0\xd23\xe4\xf2K\xccM\xb5RH\xcb\xcf/HL\xceNLO\xe5\x82\xcb\x1a\xe8YZ\xea\xa5\xa4\x96\x19pq' + b'\x01\x00PK\x03\x04\x14\x00\x00\x00\x08\x00Jl\x03W\xda9\xe8\xb4[\x00\x00\x00\\\x00\x00\x00$\x00\x00' + b'\x00foopackage-0.99.dev0.dist-info/WHEEL\x0b\xcfHM\xcd\xd1\rK-*\xce\xcc\xcf\xb3R0\xd43\xe0rO\xcdK-J,' + b'\xc9/\xb2RHJ\xc9,.\x89/\x07\xa9Q\xd00\xd03\x01Jkr\x05\xe5\xe7\x97\xe8z\x16\xeb\x06\x94\x16\xa5\xe6' + b'd&Y)\x94\x14\x95\xa6r\x85$\xa6[)\x14T\x1a\xeb\xe6\xe5\xe7\xa5\xea&\xe6Urq\x01\x00PK\x03\x04\x14\x00' + b'\x00\x00\x08\x00Jl\x03WI*\x9e\xa7\r\x00\x00\x00\x0b\x00\x00\x00,\x00\x00\x00foopackage-0.99.dev0' + b'.dist-info/top_level.txtK\xcb\xcf/HL\xceNLO\xe5\x02\x00PK\x03\x04\x14\x00\x00\x00\x08\x00Jl\x03W' + b'\x1e\xbaW\xb5\x00\x01\x00\x00\x91\x01\x00\x00%\x00\x00\x00foopackage-0.99.dev0.dist-info/RECORD\x85' + b'\xcd\xbbv\x820\x00\x00\xd0\xddo\t\x18\xe4\x08d\xe8\x80\x88"\xf2\xb0T\xe4\xb1\xe4\x08\x06B\xa1\x064F' + b'\xe8\xd7w\xb2\xab?po\xc5X\x7f.\xdbsM\xe6\x187\xd7\x86c,\xf7\x13\xb8\xd3\xf3b\xa9}d\x98\x90\xc1\n\xbc' + b'[m\xea\x0fI\x848\xda\xb1\x80)\xf5-D\xc7&\xcc\x9d\xe8\xa1\x1f\nj\x97\xbdZ\x02U\x9fU\xff\x98\x04e\x84' + b'\xe4\x0b\x11P\xbe4w.5\xd7\x8a\xcd}\xfbh\xae\xcd\xa3\xf9\xd2]\xb1jQ4$^?\xe6\xd9\xe4C\xb6\xdfdE3\x89' + b'\xb1m\x8dt0\xb2.6s[B\xbb_-\x03K\xf4NO\x1c\xdb\xf6^\xb4\xc9W[\xed+\xf5\xd4\xfd\x06\x0b\x18\x8c^\x05' + b'\t\x9dN!\x85%\xeb.\x92[\xb8Y\x1al\xd9\xcd\xd2>\x01Z\xbc\xa39\xebqG\x04\xe9d>\xf2W\x11\xd7\x10\xeb' + b'\xca\x83\xbb\t\xf3\xa9\xf33\t5\x7f\xfa\x90\xd2\xe2\x04}\x9eW\xb5\xee\xe2\xefx\x07\x0f\xced\x00EyWD' + b'\xb6\x15Fk\x00f\x7fPK\x01\x02\x14\x03\x14\x00\x00\x00\x08\x00\nl\x03W !Z\xfc%\x00\x00\x00%\x00\x00' + b'\x00\x16\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00foopackage/__init__.py' + b'PK\x01\x02\x14\x03\x14\x00\x00\x00\x08\x00Jl\x03W\xb4wO\x876\x00\x00\x00;\x00\x00\x00\'\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81Y\x00\x00\x00foopackage-0.99.dev0.dist-info/METADATAPK\x01' + b'\x02\x14\x03\x14\x00\x00\x00\x08\x00Jl\x03W\xda9\xe8\xb4[\x00\x00\x00\\\x00\x00\x00$\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd4\x00\x00\x00foopackage-0.99.dev0.dist-info/WHEELPK\x01\x02' + b'\x14\x03\x14\x00\x00\x00\x08\x00Jl\x03WI*\x9e\xa7\r\x00\x00\x00\x0b\x00\x00\x00,\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xa4\x81q\x01\x00\x00foopackage-0.99.dev0.dist-info/top_level.txtPK\x01' + b'\x02\x14\x03\x14\x00\x00\x00\x08\x00Jl\x03W\x1e\xbaW\xb5\x00\x01\x00\x00\x91\x01\x00\x00%\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81\xc8\x01\x00\x00foopackage-0.99.dev0.dist-info/RECORDPK' + b'\x05\x06\x00\x00\x00\x00\x05\x00\x05\x00\x98\x01\x00\x00\x0b\x03\x00\x00\x00\x00') + + return self.dump_package(whl, 'foopackage-0.99.dev0-py3-none-any.whl') + + +class TestPythonInstall(BasePythonInstall): def setUp(self): # type: () -> None if os.path.isdir(PYTHON_DIR): @@ -95,5 +221,100 @@ class TestCustomPythonPathInstall(TestPythonInstall): self.assertIn(self.CUSTOM_PYTHON_DIR, output) +class TestCheckPythonDependencies(BasePythonInstall): + def setUp(self): # type: () -> None + if os.path.isdir(PYTHON_DIR): + shutil.rmtree(PYTHON_DIR) + + def tearDown(self): # type: () -> None + if os.path.isdir(PYTHON_DIR): + shutil.rmtree(PYTHON_DIR) + + def test_check_python_dependencies(self): # type: () -> None + # Prepare artificial constraints file containing packages from + # requirements.core.txt, which are also reported in pip-freeze output + # for virtual env. The constraints file requires package versions higher + # than currently installed in venv, so check_python_dependencies + # should fail for all of them. + self.run_idf_tools(['install-python-env']) + freeze_output = self.run_in_venv(['-m', 'pip', 'freeze', '--all']) + + req_fn = os.path.join(IDF_PATH, 'tools', 'requirements', 'requirements.core.txt') + with open(req_fn) as fd: + req_list = [i for i in fd.read().splitlines() if i and i[0] != '#'] + + # Create constrains list for packages in requirements.core.txt which + # are also present in the freeze list. + con_list = [r.replace('==', '>') for r in freeze_output.splitlines() if r.split('==')[0] in req_list] + + con_fn = idf_tools.get_constraints(idf_tools.get_idf_version(), online=False) + # delete modified constraints file after this test is finished + self.addCleanup(os.remove, con_fn) + + # Write the created constraints list into existing constraints file. + # It will not be overwritten by subsequent idf_tools.py run, because + # there is timestamp check. + with open(con_fn, 'w') as fd: + fd.write(os.linesep.join(con_list)) + + # Test that check_python_dependencies reports that requirements are not satisfied for + # all packages in the artificially created constrains file. + output = self.run_idf_tools(['check-python-dependencies']) + for con in [c.split('>')[0] for c in con_list]: + self.assertIn(con, output) + + def test_check_required_packages_only(self): # type: () -> None + # Test for espressif/esp-idf/-/merge_requests/17917 + # Install python env with core requirements, plus foopackage. + # Add foopackage to constraints file requiring higher version + # than currently installed. Since foopackage is not a direct + # requirement, the dependency check should ignore it and should + # not fail. + self.run_idf_tools(['install-python-env']) + foo_pkg = self.dump_foopackage() + self.run_in_venv(['-m', 'pip', 'install', foo_pkg]) + + con_fn = idf_tools.get_constraints(idf_tools.get_idf_version(), online=False) + # delete modified constraints file after this test is finished + self.addCleanup(os.remove, con_fn) + + # append foopackage constraint to the existing constraints file + with open(con_fn, 'a') as fd: + fd.write('foopackage>0.99') + + # check-python-dependencies should not complain about dummy_package + output = self.run_idf_tools(['check-python-dependencies']) + self.assertIn(REQ_SATISFIED, output) + + def test_dev_version(self): # type: () -> None + # Install python env with core requirements, plus foopackage in dev version. + # Add foopackage to constraints file meeting requirement + # Dependency check should pass as the requirement was met + # Change dependency to require dev version + # Dependency check should pass again + self.run_idf_tools(['install-python-env']) + foo_pkg = self.dump_foopackage_dev() + self.run_in_venv(['-m', 'pip', 'install', foo_pkg]) + + con_fn = idf_tools.get_constraints(idf_tools.get_idf_version(), online=False) + # delete modified constraints file after this test is finished + self.addCleanup(os.remove, con_fn) + + # append foopackage constraint to the existing constraints file + with open(con_fn, 'r+') as fd: + con_lines = fd.readlines() + fd.write('foopackage~=0.98') + + output = self.run_idf_tools(['check-python-dependencies']) + self.assertIn(REQ_SATISFIED, output) + + # append foopackage dev version constraint to the existing constraints file + with open(con_fn, 'r+') as fd: + fd.writelines(con_lines + ['foopackage==0.99.dev0']) + + output = self.run_idf_tools(['check-python-dependencies']) + self.assertIn(REQ_SATISFIED, output) + + if __name__ == '__main__': unittest.main()