diff --git a/.github/ci-hpc-config.yml b/.github/ci-hpc-config.yml new file mode 100644 index 0000000..2e20c20 --- /dev/null +++ b/.github/ci-hpc-config.yml @@ -0,0 +1,3 @@ +build: + python: 3.10 + parallel: 1 diff --git a/.github/workflows/cd-pypi.yml b/.github/workflows/cd-pypi.yml new file mode 100644 index 0000000..6104026 --- /dev/null +++ b/.github/workflows/cd-pypi.yml @@ -0,0 +1,11 @@ +name: cd + +on: + push: + tags: + - '**' + +jobs: + pypi: + uses: ecmwf-actions/reusable-workflows/.github/workflows/cd-pypi.yml@v2 + secrets: inherit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e6f7c38 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: ci + +on: + # Trigger the workflow on push to master or develop, except tag creation + push: + branches: + - 'master' + - 'develop' + tags-ignore: + - '**' + + # Trigger the workflow on pull request + pull_request: + + # Trigger the workflow manually + workflow_dispatch: + + # Trigger after public PR approved for CI + pull_request_target: + types: [labeled] + +jobs: + # Run CI including downstream packages on self-hosted runners + downstream-ci: + name: downstream-ci + if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }} + uses: ecmwf-actions/downstream-ci/.github/workflows/downstream-ci.yml@main + with: + findlibs: ecmwf/findlibs@${{ github.event.pull_request.head.sha || github.sha }} + codecov_upload: true + python_qa: true + secrets: inherit + + + # Build downstream packages on HPC + downstream-ci-hpc: + name: downstream-ci-hpc + if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }} + uses: ecmwf-actions/downstream-ci/.github/workflows/downstream-ci-hpc.yml@main + with: + findlibs: ecmwf/findlibs@${{ github.event.pull_request.head.sha || github.sha }} + secrets: inherit diff --git a/.github/workflows/label-public-pr.yml b/.github/workflows/label-public-pr.yml new file mode 100644 index 0000000..59b2bfa --- /dev/null +++ b/.github/workflows/label-public-pr.yml @@ -0,0 +1,10 @@ +# Manage labels of pull requests that originate from forks +name: label-public-pr + +on: + pull_request_target: + types: [opened, synchronize] + +jobs: + label: + uses: ecmwf-actions/reusable-workflows/.github/workflows/label-pr.yml@v2 diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml deleted file mode 100644 index 472fccf..0000000 --- a/.github/workflows/python-publish.yml +++ /dev/null @@ -1,31 +0,0 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - -name: Upload Python Package - -on: - release: - types: [created] - -jobs: - deploy: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist - twine upload dist/* diff --git a/.gitignore b/.gitignore index 566e5a1..152ddae 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,23 @@ __pycache__/ *.pyc ?.* *.egg-info/ + +### VisualStudioCode ### +.vscode/ + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope +.vscode/*.code-snippets + +# Ignore code-workspaces +*.code-workspace \ No newline at end of file diff --git a/findlibs/__init__.py b/findlibs/__init__.py index 86d6d73..519ced2 100644 --- a/findlibs/__init__.py +++ b/findlibs/__init__.py @@ -8,10 +8,10 @@ # nor does it submit to any jurisdiction. # +import configparser import ctypes.util import os import sys -import configparser from pathlib import Path __version__ = "0.0.5" @@ -21,38 +21,53 @@ "win32": ".dll", } + def _get_paths_from_config(): - locations = [Path(p).expanduser() for p in [ - "~/.config/findlibs/findlibs.conf", - "~/.findlibs", - ]] + locations = [ + Path(p).expanduser() + for p in [ + "~/.config/findlibs/findlibs.conf", + "~/.findlibs", + ] + ] locations = [p for p in locations if p.exists()] - if len(locations) == 0: return [] - if len(locations) > 1: - raise ValueError(f"There are multiple config files! Delete all but one of {locations}") + if len(locations) == 0: + return [] + if len(locations) > 1: + raise ValueError( + f"There are multiple config files! Delete all but one of {locations}" + ) - config = configparser.RawConfigParser(allow_no_value = True) # Allow keys without values - config.optionxform = lambda option: option # Preserve case of keys - - with open(locations[0], "r") as f: + config = configparser.RawConfigParser( + allow_no_value=True + ) # Allow keys without values + config.optionxform = lambda option: option # Preserve case of keys + + with open(locations[0], "r"): config.read(locations[0]) - - if "Paths" not in config: return [] - # replace $HOME with ~, expand ~ to full path, - # resolve any relative paths to absolute paths - paths = {Path(p.replace("$HOME", "~")).expanduser() - for p in config["Paths"] or []} - + + if "Paths" not in config: + return [] + # replace $HOME with ~, expand ~ to full path, + # resolve any relative paths to absolute paths + paths = {Path(p.replace("$HOME", "~")).expanduser() for p in config["Paths"] or []} + relative_paths = [p for p in paths if not p.is_absolute()] if relative_paths: - raise ValueError(f"Don't use relative paths in the config file ({locations[0]}), offending paths are: {relative_paths}") - + raise ValueError( + ( + f"Don't use relative paths in the config file ({locations[0]})," + f" offending paths are: {relative_paths}" + ) + ) + files = [p for p in paths if not p.is_dir()] if files: - raise ValueError(f"Don't put files in the config file ({locations[0]}), offending files are: {files}") - + raise ValueError( + f"Don't put files in the config file ({locations[0]}), offending files are: {files}" + ) return paths @@ -101,18 +116,17 @@ def find(lib_name, pkg_name=None): if env in os.environ: home = os.path.expanduser(os.environ[env]) for lib in ("lib", "lib64"): - fullname = os.path.join( - home, lib, libname - ) + fullname = os.path.join(home, lib, libname) if os.path.exists(fullname): return fullname config_paths = _get_paths_from_config() for root in config_paths: - for lib in ("lib", "lib64"): + for lib in ("lib", "lib64"): filepath = root / lib / f"lib{lib_name}{extension}" - if filepath.exists(): return str(filepath) + if filepath.exists(): + return str(filepath) for path in ( "LD_LIBRARY_PATH", @@ -123,7 +137,14 @@ def find(lib_name, pkg_name=None): if os.path.exists(fullname): return fullname - for root in ("/", "/usr/", "/usr/local/", "/opt/", "/opt/homebrew/", os.path.expanduser("~/.local")): + for root in ( + "/", + "/usr/", + "/usr/local/", + "/opt/", + "/opt/homebrew/", + os.path.expanduser("~/.local"), + ): for lib in ("lib", "lib64"): fullname = os.path.join(root, lib, libname) if os.path.exists(fullname): diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..4736e11 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 110 +extend-ignore = E203, W503 diff --git a/setup.py b/setup.py index 332dcdd..800d469 100644 --- a/setup.py +++ b/setup.py @@ -42,9 +42,7 @@ def read(fname): packages=setuptools.find_packages(), include_package_data=True, install_requires=[], - extras_require={ - 'test': ["pytest", "pyfakefs"] - }, + extras_require={"test": ["pytest", "pyfakefs"]}, zip_safe=True, keywords="tool", classifiers=[ diff --git a/tests/downstream-ci-requirements.txt b/tests/downstream-ci-requirements.txt new file mode 100644 index 0000000..af79207 --- /dev/null +++ b/tests/downstream-ci-requirements.txt @@ -0,0 +1,3 @@ +pytest +pytest-cov +pyfakefs \ No newline at end of file diff --git a/tests/test_basic.py b/tests/test_basic.py index abe7c5f..2d00d03 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -7,18 +7,20 @@ # granted to it by virtue of its status as an intergovernmental organisation # nor does it submit to any jurisdiction. -import pytest -import pyfakefs # registers a fixture called "fs" with pytest -import sys import os -import findlibs +import sys from pathlib import Path +import pyfakefs # noqa registers a fixture called "fs" with pytest +import pytest + +import findlibs + pkg_name = "foobar" extension = findlibs.EXTENSIONS.get(sys.platform, ".so") libname = f"lib{pkg_name}{extension}" -conda_prefix = '/test/conda/prefix' +conda_prefix = "/test/conda/prefix" os.environ["CONDA_PREFIX"] = conda_prefix env_variable_location = os.environ[f"{pkg_name}_HOME"] = "/test/environment/variable" ld_library_location = os.environ["LD_LIBRARY_PATH"] = "/test/ld_library/" @@ -30,13 +32,18 @@ sys.prefix, conda_prefix, env_variable_location, - "/", "/usr/", "/usr/local/", "/opt/", "/opt/homebrew/", os.path.expanduser("~/.local") + "/", + "/usr/", + "/usr/local/", + "/opt/", + "/opt/homebrew/", + os.path.expanduser("~/.local"), ] + @pytest.mark.parametrize("location", test_locations) def test_find(fs, location): libpath = Path(location) / "lib" / libname print(f"creating {libpath}") fs.create_file(libpath) assert findlibs.find(pkg_name) == str(libpath) - \ No newline at end of file diff --git a/tests/test_config_file.py b/tests/test_config_file.py index 7fcc19b..4edaf9c 100644 --- a/tests/test_config_file.py +++ b/tests/test_config_file.py @@ -7,81 +7,108 @@ # granted to it by virtue of its status as an intergovernmental organisation # nor does it submit to any jurisdiction. -import pytest -import pyfakefs # registers a fixture called "fs" with pytest import sys -import findlibs from pathlib import Path +import pyfakefs # noqa registers a fixture called "fs" with pytest +import pytest + +import findlibs + extension = findlibs.EXTENSIONS.get(sys.platform, ".so") testlib_path = f"/usr/lib/libtest{extension}" -config_paths = [str(Path(p).expanduser()) for p in [f"~/.config/findlibs/findlibs.conf", - f"~/.findlibs"]] +config_paths = [ + str(Path(p).expanduser()) + for p in ["~/.config/findlibs/findlibs.conf", "~/.findlibs"] +] + def test_no_config_file(fs): "Check that findlibs works with no config file" fs.create_file(testlib_path) assert findlibs.find("test") == testlib_path + def test_both_config_files(fs): "Check that it throws an error if two config files are present" - for p in config_paths: fs.create_file(p) + for p in config_paths: + fs.create_file(p) with pytest.raises(ValueError): findlibs._get_paths_from_config() + def test_relative_path(fs): "Check that it throws an error on relative paths" - fs.create_file(config_paths[0], contents = -""" + fs.create_file( + config_paths[0], + contents=""" [Paths] relative/path -""") +""", + ) with pytest.raises(ValueError): findlibs._get_paths_from_config() + def test_file(fs): "Check that it throws an error when files are included" - fs.create_file(config_paths[0], contents = -""" + fs.create_file( + config_paths[0], + contents=""" [Paths] /path/to/file.so -""") +""", + ) with pytest.raises(ValueError): findlibs._get_paths_from_config() + def test_empty_config_file(fs): "Check that it works with an empty config file" fs.create_file(testlib_path) fs.create_file(config_paths[0]) assert findlibs.find("test") == testlib_path + def test_empty_search_paths(fs): "Check that it works with an empty config file" fs.create_file(testlib_path) - fs.create_file(config_paths[0], contents = -""" + fs.create_file( + config_paths[0], + contents=""" [Paths] -""") +""", + ) assert findlibs.find("test") == testlib_path + # Parametrised tests over # search_dir: a path you could put into .findlibs.yml # testlib_path: a concrete test path to check we can find tings in search_dir -@pytest.mark.parametrize("search_dir,testlib_path", [ - ("/test/usr", f"/test/usr/lib/libtest{extension}"), # absolute paths - ("/test/usr", f"/test/usr/lib64/libtest{extension}"), # lib64 aswell as lib - ("~/.local", Path(f"~/.local/lib/libtest{extension}").expanduser()), # ~ expansion - ("$HOME/.local", Path(f"~/.local/lib/libtest{extension}").expanduser()), # $HOME expansion -]) +@pytest.mark.parametrize( + "search_dir,testlib_path", + [ + ("/test/usr", f"/test/usr/lib/libtest{extension}"), # absolute paths + ("/test/usr", f"/test/usr/lib64/libtest{extension}"), # lib64 aswell as lib + ( + "~/.local", + Path(f"~/.local/lib/libtest{extension}").expanduser(), + ), # ~ expansion + ( + "$HOME/.local", + Path(f"~/.local/lib/libtest{extension}").expanduser(), + ), # $HOME expansion + ], +) def test_config(fs, search_dir, testlib_path): - fs.create_file(testlib_path) - fs.create_file(config_paths[0], contents = -f""" + fs.create_file( + config_paths[0], + contents=f""" [Paths] {search_dir} -""") +""", + ) assert findlibs.find("test") == str(testlib_path) - \ No newline at end of file