diff --git a/.github/workflows/nbdev_tests.yml b/.github/workflows/nbdev_tests.yml index b7b9d39..73c2e5e 100644 --- a/.github/workflows/nbdev_tests.yml +++ b/.github/workflows/nbdev_tests.yml @@ -21,16 +21,16 @@ jobs: python-version: ${{ matrix.python-version }} miniconda-version: latest - name: Conda info - shell: bash -l {0} + shell: bash -le {0} run: conda info - name: Test pip installation with all loose dependencies - shell: bash -l {0} + shell: bash -le {0} run: | cd misc . ./loose_pip_install.sh - name: Unittests - shell: bash -l {0} + shell: bash -le {0} run: | conda activate directlfq nbdev_test diff --git a/.github/workflows/quick_tests.yml b/.github/workflows/quick_tests.yml index a1a4f8b..aab6ff6 100644 --- a/.github/workflows/quick_tests.yml +++ b/.github/workflows/quick_tests.yml @@ -21,15 +21,20 @@ jobs: python-version: ${{ matrix.python-version }} miniconda-version: latest - name: Conda info - shell: bash -l {0} + shell: bash -le {0} run: conda info + - name: Pytest tests + shell: bash -le {0} + run: | + pip install pytest + pytest tests/pytest - name: Test pip installation with all stable dependencies - shell: bash -l {0} + shell: bash -le {0} run: | cd misc . ./stable_pip_install.sh - name: Run pipeline - shell: bash -l {0} + shell: bash -le {0} run: | cd tests . ./run_quicktests.sh @@ -47,15 +52,20 @@ jobs: python-version: ${{ matrix.python-version }} miniconda-version: latest - name: Conda info - shell: bash -l {0} + shell: bash -le {0} run: conda info + - name: Pytest tests + shell: bash -le {0} + run: | + pip install pytest + pytest tests/pytest - name: Test pip installation with all loose dependencies - shell: bash -l {0} + shell: bash -le {0} run: | cd misc . ./loose_pip_install.sh - name: Run pipeline - shell: bash -l {0} + shell: bash -le {0} run: | cd tests . ./run_quicktests.sh diff --git a/.github/workflows/quick_tests_ubuntu.yml b/.github/workflows/quick_tests_ubuntu.yml index 952c2fa..9b046bf 100644 --- a/.github/workflows/quick_tests_ubuntu.yml +++ b/.github/workflows/quick_tests_ubuntu.yml @@ -21,16 +21,16 @@ jobs: python-version: ${{ matrix.python-version }} miniconda-version: latest - name: Conda info - shell: bash -l {0} + shell: bash -le {0} run: conda info - name: Test pip installation with stable dependencies - shell: bash -l {0} + shell: bash -le {0} run: | cd misc . ./stable_pip_install.sh - name: Unittests - shell: bash -l {0} + shell: bash -le {0} run: | cd tests . ./run_quicktests.sh diff --git a/.github/workflows/ratio_tests.yml b/.github/workflows/ratio_tests.yml index 1b7b6a2..9f44bc4 100644 --- a/.github/workflows/ratio_tests.yml +++ b/.github/workflows/ratio_tests.yml @@ -19,16 +19,16 @@ jobs: python-version: ${{ matrix.python-version }} miniconda-version: latest - name: Conda info - shell: bash -l {0} + shell: bash -le {0} run: conda info - name: Test pip installation with stable dependencies - shell: bash -l {0} + shell: bash -le {0} run: | cd misc . ./stable_pip_install.sh - name: Ratio tests - shell: bash -l {0} + shell: bash -le {0} run: | cd tests . ./run_ratio_tests.sh diff --git a/.github/workflows/unused/all_tests.yml b/.github/workflows/unused/all_tests.yml index 454bd9a..0fce3f9 100644 --- a/.github/workflows/unused/all_tests.yml +++ b/.github/workflows/unused/all_tests.yml @@ -22,15 +22,15 @@ jobs: python-version: ${{ matrix.python-version }} miniconda-version: latest - name: Conda info - shell: bash -l {0} + shell: bash -le {0} run: conda info - name: Test pip installation with all loose dependencies - shell: bash -l {0} + shell: bash -le {0} run: | cd misc . ./loose_pip_install.sh - name: Unittests - shell: bash -l {0} + shell: bash -le {0} run: | cd tests . ./run_tests.sh diff --git a/README.md b/README.md index a3830a3..9c623a6 100644 --- a/README.md +++ b/README.md @@ -91,10 +91,10 @@ pip install "directlfq[stable]" NOTE: You might need to run `pip install pip==21.0` before installing directlfq like this. Also note the double quotes `"`. -For those who are really adventurous, it is also possible to directly install any branch (e.g. `@development`) with any extras (e.g. `#egg=directlfq[stable,development-stable]`) from GitHub with e.g. +For those who are really adventurous, it is also possible to directly install any branch (e.g. `@development`) with any extras (e.g. `#egg=directlfq[stable,development]`) from GitHub with e.g. ```bash -pip install "git+https://github.com/MannLabs/directlfq.git@development#egg=directlfq[stable,development-stable]" +pip install "git+https://github.com/MannLabs/directlfq.git@development#egg=directlfq[stable,development]" ``` ### Developer @@ -127,7 +127,7 @@ Finally, directlfq and all its [dependencies](requirements) need to be installed pip install -e "./directlfq[development,gui]" ``` -By default this installs loose dependencies (no explicit versioning), although it is also possible to use stable dependencies (e.g. `pip install -e "./directlfq[stable,development-stable]"`). +By default this installs loose dependencies (no explicit versioning), although it is also possible to use stable dependencies (e.g. `pip install -e "./directlfq[stable,development]"`). ***By using the editable flag `-e`, all modifications to the [directlfq source code folder](directlfq) are directly reflected when running directlfq. Note that the directlfq folder cannot be moved and/or renamed if an editable version is installed. In case of confusion, you can always retrieve the location of any Python module with e.g. the command `import module` followed by `module.__file__`.*** diff --git a/directlfq/__init__.py b/directlfq/__init__.py index 6730f2e..c18508e 100644 --- a/directlfq/__init__.py +++ b/directlfq/__init__.py @@ -1,45 +1 @@ -#!python - - -__project__ = "directlfq" -__version__ = "0.3.1-dev0" -__license__ = "Apache" -__description__ = "An open-source Python package of the AlphaPept ecosystem" -__author__ = "Mann Labs" -__author_email__ = "opensource@alphapept.com" -__github__ = "https://github.com/MannLabs/directlfq" -__keywords__ = [ - "bioinformatics", - "software", - "AlphaPept ecosystem", -] -__python_version__ = ">=3.9" -__classifiers__ = [ - # "Development Status :: 1 - Planning", - # "Development Status :: 2 - Pre-Alpha", - # "Development Status :: 3 - Alpha", - # "Development Status :: 4 - Beta", - "Development Status :: 5 - Production/Stable", - # "Development Status :: 6 - Mature", - # "Development Status :: 7 - Inactive" - "Intended Audience :: Science/Research", - "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Topic :: Scientific/Engineering :: Bio-Informatics", -] -__console_scripts__ = [ - "directlfq=directlfq.cli:run", -] -__urls__ = { - "Mann Labs at MPIB": "https://www.biochem.mpg.de/mann", - "Mann Labs at CPR": "https://www.cpr.ku.dk/research/proteomics/mann/", - "GitHub": __github__, - # "ReadTheDocs": None, - # "PyPi": None, - # "Scientific paper": None, -} -__extra_requirements__ = { - "development": "requirements_development.txt", - "gui": "requirements_gui.txt" -} \ No newline at end of file +__version__ = "0.3.1-dev0" \ No newline at end of file diff --git a/misc/loose_pip_install.sh b/misc/loose_pip_install.sh index d7ba47b..64e5fcf 100644 --- a/misc/loose_pip_install.sh +++ b/misc/loose_pip_install.sh @@ -1,5 +1,5 @@ conda create -n directlfq python=3.9 -y conda activate directlfq -pip install -e '../.[development-stable, gui]' +pip install -e '../.[development, gui]' directlfq conda deactivate diff --git a/misc/stable_pip_install.sh b/misc/stable_pip_install.sh index 6657dda..69dedb2 100644 --- a/misc/stable_pip_install.sh +++ b/misc/stable_pip_install.sh @@ -1,5 +1,5 @@ conda create -n directlfq python=3.9 -y conda activate directlfq -pip install -e '../.[stable,development-stable, gui-stable]' +pip install -e '../.[stable, gui-stable, development]' directlfq conda deactivate diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0e51b32 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,60 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "directlfq" +requires-python = ">=3.9" +dynamic = ["version", "dependencies", "optional-dependencies"] + +authors = [ + {name = "Mann Labs", email = "opensource@alphapept.com"} +] +description = "An open-source Python package of the AlphaPept ecosystem" +readme = "README.md" +keywords = [ + "LFQ", + "label-free quantification", + "mass spectrometry", + "proteomics", + "bioinformatics", + "AlphaPept", + "AlphaPept ecosystem", +] +license = {file = "LICENSE.txt"} +classifiers = [ + "Development Status :: 5 - Production/Stable", + # "Development Status :: 6 - Mature", + # "Development Status :: 7 - Inactive" + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering :: Bio-Informatics", +] + +[project.urls] + +"Paper preprint" = "https://www.biochem.mpg.de/mann" +Repository = "https://github.com/MannLabs/directlfq" +#Documentation = "https://readthedocs.org" +#Changelog = "https://github.com/me/spam/blob/master/CHANGELOG.md" +Issues = "https://github.com/MannLabs/directlfq/issues" +"Mann Labs Homepage" = "https://www.biochem.mpg.de/mann" + +[tool.setuptools.packages] +find = {} + +[tool.setuptools.dynamic] +# https://stackoverflow.com/a/73600610 +dependencies = {file = ["requirements/requirements_loose.txt"]} +optional-dependencies = { stable = { file = ["requirements/requirements.txt", +] }, gui = { file = [ "requirements/requirements_gui_loose.txt", +] }, gui-stable = { file = [ "requirements/requirements_gui.txt", +] }, development = { file = ["requirements/requirements_development.txt" +] }} + +version = {attr = "directlfq.__version__"} + +[project.scripts] +directlfq = "directlfq.cli:run" \ No newline at end of file diff --git a/release/linux/build_installer_linux.sh b/release/linux/build_installer_linux.sh index 898c1cf..18e75c1 100755 --- a/release/linux/build_installer_linux.sh +++ b/release/linux/build_installer_linux.sh @@ -8,8 +8,11 @@ rm -rf dist build *.egg-info rm -rf dist_pyinstaller build_pyinstaller # Creating the wheel -python setup.py sdist bdist_wheel -pip install "dist/directlfq-0.3.1-dev0-py3-none-any.whl[stable,gui]" +python -m build + +# substitute X.Y.Z-devN with X.Y.Z.devN +WHL_NAME=$(echo "directlfq-0.3.1-dev0-py3-none-any.whl" | sed 's/\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)-dev\([0-9][0-9]*\)/\1.dev\2/g') +pip install "dist/${WHL_NAME}[stable,gui-stable]" # Creating the stand-alone pyinstaller folder pip install pyinstaller diff --git a/release/macos/build_installer_macos.sh b/release/macos/build_installer_macos.sh index 9f006a5..295e156 100755 --- a/release/macos/build_installer_macos.sh +++ b/release/macos/build_installer_macos.sh @@ -8,8 +8,11 @@ rm -rf dist build *.egg-info rm -rf dist_pyinstaller build_pyinstaller # Creating the wheel -python setup.py sdist bdist_wheel -pip install "dist/directlfq-0.3.1-dev0-py3-none-any.whl[stable,gui]" +python -m build + +# substitute X.Y.Z-devN with X.Y.Z.devN +WHL_NAME=$(echo "directlfq-0.3.1-dev0-py3-none-any.whl" | sed 's/\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)-dev\([0-9][0-9]*\)/\1.dev\2/g') +pip install "dist/${WHL_NAME}[stable,gui-stable]" # Creating the stand-alone pyinstaller folder pip install pyinstaller diff --git a/release/pyinstaller/directlfq.spec b/release/pyinstaller/directlfq.spec index 84f8d3a..65ee566 100644 --- a/release/pyinstaller/directlfq.spec +++ b/release/pyinstaller/directlfq.spec @@ -1,13 +1,9 @@ # -*- mode: python ; coding: utf-8 -*- -import pkgutil import os import sys from PyInstaller.building.build_main import Analysis, PYZ, EXE, COLLECT, BUNDLE, TOC import PyInstaller.utils.hooks -import pkg_resources -import importlib.metadata -import directlfq ##################### User definitions @@ -20,59 +16,31 @@ else: block_cipher = None location = os.getcwd() project = "directlfq" -remove_tests = True bundle_name = "directlfq" ##################### -requirements = { - req.split()[0] for req in importlib.metadata.requires(project) -} -requirements.add(project) -requirements.add("distributed") -hidden_imports = set() -datas = [] -binaries = [] -checked = set() -while requirements: - requirement = requirements.pop() - checked.add(requirement) - if requirement in ["pywin32"]: - continue - try: - module_version = importlib.metadata.version(requirement) - except ( - importlib.metadata.PackageNotFoundError, - ModuleNotFoundError, - ImportError - ): - continue - try: - datas_, binaries_, hidden_imports_ = PyInstaller.utils.hooks.collect_all( - requirement, - include_py_files=True - ) - except ImportError: - continue - datas += datas_ - # binaries += binaries_ - hidden_imports_ = set(hidden_imports_) - if "" in hidden_imports_: - hidden_imports_.remove("") - if None in hidden_imports_: - hidden_imports_.remove(None) - requirements |= hidden_imports_ - checked - hidden_imports |= hidden_imports_ - -if remove_tests: - hidden_imports = sorted( - [h for h in hidden_imports if "tests" not in h.split(".")] - ) -else: - hidden_imports = sorted(hidden_imports) +datas, binaries, hidden_imports = PyInstaller.utils.hooks.collect_all( + project, + include_py_files=True +) +# add extra packages that don't have pyinstaller hooks +# extra_pkgs = ["alphabase", ] # other alphaX packages would be added here +# for pkg in extra_pkgs: +# _datas, _binaries, _hidden_imports = PyInstaller.utils.hooks.collect_all( +# pkg, +# include_py_files=True +# ) +# datas+=_datas +# binaries+=_binaries +# hidden_imports+=_hidden_imports +# prepare hidden imports and datas hidden_imports = [h for h in hidden_imports if "__pycache__" not in h] +# hidden_imports = sorted( +# [h for h in hidden_imports if "tests" not in h.split(".")] +# ) datas = [d for d in datas if ("__pycache__" not in d[0]) and (d[1] not in [".", "Resources", "scripts"])] a = Analysis( @@ -111,7 +79,7 @@ if sys.platform[:5] == "linux": upx_exclude=[], icon=icon ) -else: +else: # non-linux exe = EXE( pyz, a.scripts, diff --git a/release/pyinstaller/directlfq_pyinstaller.py b/release/pyinstaller/directlfq_pyinstaller.py index 7baedb8..c056dd2 100644 --- a/release/pyinstaller/directlfq_pyinstaller.py +++ b/release/pyinstaller/directlfq_pyinstaller.py @@ -4,7 +4,7 @@ import multiprocessing multiprocessing.freeze_support() directlfq.gui.run() - except e: + except Exception as e: import traceback import sys exc_info = sys.exc_info() diff --git a/release/windows/build_installer_windows.ps1 b/release/windows/build_installer_windows.ps1 index babff85..f552c1c 100644 --- a/release/windows/build_installer_windows.ps1 +++ b/release/windows/build_installer_windows.ps1 @@ -8,9 +8,12 @@ Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ./build_pyinstaller Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ./dist_pyinstaller # Creating the wheel -python setup.py sdist bdist_wheel -# Make sure you include the required extra packages and always use the stable or very-stable options! -pip install "dist/directlfq-0.3.1-dev0-py3-none-any.whl[stable, gui]" +python -m build +# Make sure you include the required extra packages and always use the stable options! + +# substitute X.Y.Z-devN with X.Y.Z.devN +$WHL_NAME = "directlfq-0.3.1-dev0-py3-none-any.whl" -replace '(\d+\.\d+\.\d+)-dev(\d+)', '$1.dev$2' +pip install "dist/$WHL_NAME[stable,gui-stable]" # Creating the stand-alone pyinstaller folder pip install pyinstaller diff --git a/requirements/requirements.txt b/requirements/requirements.txt index dab8ae1..0df71fa 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,7 +1,10 @@ +# Dependencies required for running the "stable" version of directlfq. +# Only usage of fixed versions is allowed, and all dependencies listed here must also be +# included in `requirements_loose.txt` (enforced by a test). Jinja2==3.1.2 numpy==1.23.5 -pandas>=1.5.3 -dask>=2023.1.0 +pandas>=1.5.3 # test: tolerate_version +dask>=2023.1.0 # test: tolerate_version numba==0.56.4 multiprocess==0.70.14 wget==3.2 diff --git a/requirements/requirements_development.txt b/requirements/requirements_development.txt index 688b765..f9e2ca3 100644 --- a/requirements/requirements_development.txt +++ b/requirements/requirements_development.txt @@ -16,4 +16,5 @@ matplotlib nbdev>=2.3.9 notebook alphabase>=1.4.0 -progressbar \ No newline at end of file +progressbar +pytest \ No newline at end of file diff --git a/requirements/requirements_gui.txt b/requirements/requirements_gui.txt index b7de75e..945f5d2 100644 --- a/requirements/requirements_gui.txt +++ b/requirements/requirements_gui.txt @@ -1,3 +1,6 @@ -panel==0.10.3 -dash==2.5.1 -matplotlib==3.4.3 \ No newline at end of file +# Additional dependencies required for running the "stable" version of directlfq GUI. +# Only usage of fixed versions is allowed, and all dependencies listed here must also be +# included in `requirements_gui_loose.txt` (enforced by a test). +panel==1.4.5 +dash==2.18.1 +matplotlib==3.9.2 \ No newline at end of file diff --git a/requirements/requirements_gui_loose.txt b/requirements/requirements_gui_loose.txt new file mode 100644 index 0000000..9055409 --- /dev/null +++ b/requirements/requirements_gui_loose.txt @@ -0,0 +1,5 @@ +# Additional dependencies required for running the "loose" version of directlfq GUI. +# All dependencies that are also included in `requirements.txt` must be added also here (enforced by a test). +panel +dash +matplotlib \ No newline at end of file diff --git a/requirements/requirements_loose.txt b/requirements/requirements_loose.txt new file mode 100644 index 0000000..3ea836c --- /dev/null +++ b/requirements/requirements_loose.txt @@ -0,0 +1,11 @@ +# Dependencies required for running the "loose" version of direclfq. +# All dependencies that are also included in `requirements.txt` must be added also here (enforced by a test). +Jinja2 +numpy +pandas +dask +numba +multiprocess +wget +PyYAML +pyarrow \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 82fdb45..0000000 --- a/setup.py +++ /dev/null @@ -1,72 +0,0 @@ -#!python - -# builtin -import setuptools -import re -import os -# local -import directlfq as package2install - - -def get_long_description(): - with open("README.md", "r") as readme_file: - long_description = readme_file.read() - return long_description - - -def get_requirements(): - extra_requirements = {} - requirement_file_names = package2install.__extra_requirements__ - requirement_file_names[""] = "requirements.txt" - for extra, requirement_file_name in requirement_file_names.items(): - full_requirement_file_name = os.path.join( - "requirements", - requirement_file_name, - ) - with open(full_requirement_file_name) as requirements_file: - if extra != "": - extra_stable = f"{extra}-stable" - else: - extra_stable = "stable" - extra_requirements[extra_stable] = [] - extra_requirements[extra] = [] - for line in requirements_file: - extra_requirements[extra_stable].append(line) - requirement, *comparison = re.split("[><=~!]", line) - requirement == requirement.strip() - extra_requirements[extra].append(requirement) - requirements = extra_requirements.pop("") - return requirements, extra_requirements - - -def create_pip_wheel(): - requirements, extra_requirements = get_requirements() - setuptools.setup( - name=package2install.__project__, - version=package2install.__version__, - license=package2install.__license__, - description=package2install.__description__, - long_description=get_long_description(), - long_description_content_type="text/markdown", - author=package2install.__author__, - author_email=package2install.__author_email__, - url=package2install.__github__, - project_urls=package2install.__urls__, - keywords=package2install.__keywords__, - classifiers=package2install.__classifiers__, - packages=[package2install.__project__], - include_package_data=True, - entry_points={ - "console_scripts": package2install.__console_scripts__, - }, - install_requires=requirements + [ - # TODO Remove hardcoded requirement? - # "pywin32==225; sys_platform=='win32'" - ], - extras_require=extra_requirements, - python_requires=package2install.__python_version__, - ) - - -if __name__ == "__main__": - create_pip_wheel() diff --git a/tests/pytest/test_requirements.py b/tests/pytest/test_requirements.py new file mode 100644 index 0000000..fc24edc --- /dev/null +++ b/tests/pytest/test_requirements.py @@ -0,0 +1,131 @@ +"""Test that the strict and loose requirements files are aligned.""" + +import logging +import os +import re + +import pytest +from packaging.requirements import Requirement + +# special comment to tolerate +# - non-strict version in strict requirements file +# - defined version in loose requirements file +TOLERATE_VERSION_COMMENT = "test: tolerate_version" + + +def _split_at_first_hash(input_string: str) -> tuple[str, ...]: + """Split input string at the first occurrence of '#'. + + Always returns a tuple of two strings, even if the input string does not contain a '#'. + """ + + # (? dict[str, tuple[Requirement, str]]: + """ + Read a requirements file and return a dictionary of packages with their comments. + + Parameters + ---------- + file_path : str + The path to the requirements file to parse. + + Returns + ------- + dict: + A dictionary of packages with their comments. + The keys are the package names, and the values are tuples of the form (Requirement, str). + The str is the comment associated with the package in the requirements file. + + """ + packages = {} + with open(file_path) as file: + for line in file: + line = line.strip() + if line and not line.startswith("#"): + req_string, comment = _split_at_first_hash(line) + + req = Requirement(req_string) + if req.name in packages: + raise ValueError( + f"Duplicate package '{req.name}' found in requirements file '{file_path}'" + ) + + packages[req.name] = (req, comment) + + return packages + + +def _get_requirements_path(): + """Get the path to the requirements directory.""" + path_to_current_file = os.path.realpath(__file__) + current_directory = os.path.split(path_to_current_file)[0] + requirements_path = os.path.join(current_directory, "../../requirements/") + return requirements_path + + +@pytest.mark.parametrize("extra_name", ["", "_gui"]) +def test_requirements(extra_name): + """Test the strict and loose requirements. + + The strict requirements must have one fixed version. + + All requirements must be present in the loose requirements. + The loose requirements should not have a fixed version unless an exception is + stated by the "test:tolerate_version" comment. + """ + + file_name_strict = f"requirements{extra_name}.txt" + file_name_loose = f"requirements{extra_name}_loose.txt" + requirements_path = _get_requirements_path() + file_path_strict = os.path.join(requirements_path, file_name_strict) + file_path_loose = os.path.join(requirements_path, file_name_loose) + + reqs_strict = _read_requirements(file_path_strict) + reqs_loose = _read_requirements(file_path_loose) + + req_loose_names = reqs_loose.keys() + req_strict_names = reqs_strict.keys() + + set_loose = set(req_loose_names) + set_strict = set(req_strict_names) + assert ( + set_strict == set_loose + ), f"Requirements in do not match. only in strict: {set_strict-set_loose}; only in loose: {set_loose-set_strict}" + + for _, (req, comment) in reqs_strict.items(): + assert ( + len(req.specifier) == 1 + ), f"Requirement '{req}' does not have one defined version in '{file_name_strict}'" + + if TOLERATE_VERSION_COMMENT not in comment: + assert str( + list(req.specifier)[0] + ).startswith( + "==" + ), f"Requirement '{req}' does not have a fixed version ('==') in '{file_name_strict}'" + + for req_name, (req, comment) in reqs_loose.items(): + if TOLERATE_VERSION_COMMENT not in comment: + assert ( + len(req.specifier) == 0 + ), f"Requirement '{req}' must not have a defined version in '{file_name_loose}'" + else: + if reqs_strict[req_name][0] == req: + logging.info(f"Tolerating {req} as it's the same in both files") + continue + + # here we rely on the test for 'fixed version' above to access the specifier + specifier_strict = reqs_strict[req_name][0].specifier + version_strict = str(list(specifier_strict)[0]).replace("==", "") + + specifier_loose = req.specifier + assert specifier_loose.contains( + version_strict + ), f"Requirement '{req}' is too strict in '{file_name_loose}'"