├── vsc-ci.ini
├── test
├── data
│ └── mailconfig.ini
├── runtests
│ ├── run_nested.sh
│ ├── simple.py
│ ├── simple_option.py
│ └── qa.py
├── __init__.py
├── sandbox
│ └── testpkg
│ │ ├── __init__.py
│ │ ├── testmodule.py
│ │ └── testmodulebis.py
├── 00-import.py
├── wrapper.py
├── docs.py
├── groups.py
├── asyncprocess.py
├── dateandtime.py
├── mail.py
├── optcomplete.py
├── rest.py
├── exceptions.py
└── run.py
├── .gitignore
├── tox.ini
├── ruff.toml
├── lib
└── vsc
│ ├── __init__.py
│ └── utils
│ ├── __init__.py
│ ├── wrapper.py
│ ├── patterns.py
│ ├── frozendict.py
│ ├── docs.py
│ ├── groups.py
│ ├── daemon.py
│ ├── exceptions.py
│ ├── asyncprocess.py
│ ├── affinity.py
│ ├── dateandtime.py
│ ├── mail.py
│ ├── rest.py
│ ├── missing.py
│ └── optcomplete.py
├── Jenkinsfile
├── .github
└── workflows
│ └── unittest.yml
├── setup.py
├── examples
├── simple_option.py
└── qa.py
├── bin
└── optcomplete.bash
└── README.md
/vsc-ci.ini:
--------------------------------------------------------------------------------
1 | [vsc-ci]
2 | enable_github_actions=1
3 | py39_tests_must_pass=1
4 | run_ruff_format_check=1
5 | run_ruff_check=1
6 |
--------------------------------------------------------------------------------
/test/data/mailconfig.ini:
--------------------------------------------------------------------------------
1 | [MAIN]
2 | smtp = config_host:789
3 | smtp_auth_user = config_user
4 | smtp_auth_password = config_passwd
5 | smtp_use_starttls = 1
6 |
--------------------------------------------------------------------------------
/test/runtests/run_nested.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | depth=${1:-1}
4 | path=${2:-/dev/null}
5 |
6 | mysleep=15
7 |
8 | echo "$depth $$ $PPID" >> $path
9 |
10 | if [ $depth -ne 0 ]; then
11 | "$0" $(($depth -1)) "$path" &
12 | fi
13 |
14 | sleep $mysleep
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[co]
2 | *.swp
3 | *~
4 |
5 | # ok to ignore generated file in vsc-base
6 | setup.cfg
7 |
8 | # Packages
9 | .eggs*
10 | *.egg
11 | *.egg-info
12 | dist
13 | build
14 | eggs
15 | parts
16 | var
17 | sdist
18 | develop-eggs
19 | .installed.cfg
20 |
21 | # Installer logs
22 | pip-log.txt
23 |
24 | # Unit test / coverage reports
25 | .coverage
26 | .tox
27 |
28 | #Translations
29 | *.mo
30 |
31 | #Mr Developer
32 | .mr.developer.cfg
33 |
34 | # Ninja
35 | *.nja
36 |
37 | html
38 | test-reports
39 | htmlcov/
40 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | # tox.ini: configuration file for tox
2 | # This file was automatically generated using 'python -m vsc.install.ci'
3 | # DO NOT EDIT MANUALLY
4 |
5 | [tox]
6 | envlist = py36,py39
7 | skipsdist = true
8 |
9 | [testenv:py36]
10 | commands_pre =
11 | pip install 'setuptools<42.0'
12 | python -m easy_install -U vsc-install
13 |
14 | [testenv:py39]
15 | setenv = SETUPTOOLS_USE_DISTUTILS=local
16 | commands_pre =
17 | pip install 'setuptools<54.0' wheel
18 | python -c "from setuptools import setup;setup(script_args=['-q', 'easy_install', '-v', '-U', 'vsc-install'])"
19 |
20 | [testenv]
21 | commands = python setup.py test
22 | passenv = USER
23 |
--------------------------------------------------------------------------------
/ruff.toml:
--------------------------------------------------------------------------------
1 | line-length = 120
2 | indent-width = 4
3 | preview = true
4 | target-version = "py37"
5 | exclude = ['.bzr', '.direnv', '.eggs', '.git', '.git-rewrite', '.hg', '.ipynb_checkpoints', '.mypy_cache', '.nox', '.pants.d', '.pyenv', '.pytest_cache', '.pytype', '.ruff_cache', '.svn', '.tox', '.venv', '.vscode', '__pypackages__', '_build', 'buck-out', 'build', 'dist', 'node_modules', 'site-packages', 'venv', 'test/*']
6 | [lint]
7 | extend-select = ['E101', 'E501', 'E713', 'E4', 'E7', 'E9', 'F', 'F811', 'W291', 'PLR0911', 'PLW0602', 'PLW0604', 'PLW0108', 'PLW0127', 'PLW0129', 'PLW1501', 'PLR0124', 'PLR0202', 'PLR0203', 'PLR0402', 'PLR0913', 'B028', 'B905', 'C402', 'C403', 'UP032', 'UP037', 'UP025', 'UP026', 'UP036', 'UP034', 'UP033', 'UP031', 'UP004']
8 | ignore = ['E731']
9 | pylint.max-args = 11
10 | [format]
11 | quote-style = "double"
12 | indent-style = "space"
13 | docstring-code-format = true
14 | docstring-code-line-length = 120
15 | line-ending = "lf"
16 |
--------------------------------------------------------------------------------
/test/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2012-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Init
28 | """
29 |
--------------------------------------------------------------------------------
/test/sandbox/testpkg/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2021-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | """
28 |
--------------------------------------------------------------------------------
/lib/vsc/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2015-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Allow other packages to extend this namespace, zip safe setuptools style
28 | """
29 |
30 | import pkg_resources
31 |
32 | pkg_resources.declare_namespace(__name__)
33 |
--------------------------------------------------------------------------------
/lib/vsc/utils/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2015-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Allow other packages to extend this namespace, zip safe setuptools style
28 | """
29 |
30 | import pkg_resources
31 |
32 | pkg_resources.declare_namespace(__name__)
33 |
--------------------------------------------------------------------------------
/test/00-import.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2016-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Common test import
28 | """
29 | import vsc.install.commontest
30 |
31 |
32 | class ImportTest(vsc.install.commontest.CommonTest):
33 |
34 | # skip import for vsc.utils.py2vs3 modules
35 | EXCLUDE_MODS = [r'^vsc\.utils\.py2vs3']
36 |
--------------------------------------------------------------------------------
/test/sandbox/testpkg/testmodule.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2014-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Test module used by the unit tests
28 |
29 | @author: Kenneth Hoste (Ghent University)
30 | """
31 | class TestModA(object):
32 | pass
33 |
34 | class TestModA1(TestModA):
35 | pass
36 |
37 | class TestModA2(TestModA):
38 | pass
39 |
--------------------------------------------------------------------------------
/test/sandbox/testpkg/testmodulebis.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2014-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Test module used by the unit tests
28 |
29 | @author: Kenneth Hoste (Ghent University)
30 | """
31 | from testmodule import TestModA, TestModA1
32 | class TestModA3(TestModA):
33 | pass
34 |
35 | class TestModA1B(TestModA1):
36 | pass
37 |
38 | class TestModA1B1(TestModA1B):
39 | pass
40 |
--------------------------------------------------------------------------------
/Jenkinsfile:
--------------------------------------------------------------------------------
1 | // Jenkinsfile: scripted Jenkins pipefile
2 | // This file was automatically generated using 'python -m vsc.install.ci'
3 | // DO NOT EDIT MANUALLY
4 |
5 | pipeline {
6 | agent any
7 | stages {
8 | stage('checkout git') {
9 | steps {
10 | checkout scm
11 | // remove untracked files (*.pyc for example)
12 | sh 'git clean -fxd'
13 | }
14 | }
15 | stage('install ruff') {
16 | steps {
17 | sh 'curl -L --silent https://github.com/astral-sh/ruff/releases/download/0.13.1/ruff-x86_64-unknown-linux-gnu.tar.gz --output - | tar -xzv'
18 | sh 'cp ruff-x86_64-unknown-linux-gnu/ruff .'
19 | sh './ruff --version'
20 | }
21 | }
22 | stage('test pipeline') {
23 | parallel {
24 | stage ('ruff format') {
25 | steps {
26 | sh './ruff format --check .'
27 | }
28 | }
29 | stage ('ruff check') {
30 | steps {
31 | sh './ruff check .'
32 | }
33 | }
34 | stage('test') {
35 | steps {
36 | sh 'pip3 install --ignore-installed --prefix $PWD/.vsc-tox tox'
37 | sh 'export PATH=$PWD/.vsc-tox/bin:$PATH && export PYTHONPATH=$PWD/.vsc-tox/lib/python$(python3 -c "import sys; print(\\"%s.%s\\" % sys.version_info[:2])")/site-packages:$PYTHONPATH && tox -v -c tox.ini'
38 | sh 'rm -r $PWD/.vsc-tox'
39 | }
40 | }
41 | }
42 | }
43 | }}
44 |
--------------------------------------------------------------------------------
/test/runtests/simple.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2016-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Simple, ugly test script
28 | """
29 | import time
30 | import sys
31 |
32 | EC_SUCCES = 0
33 | EC_NOARGS = 1
34 |
35 | txt = []
36 | ec = EC_SUCCES
37 |
38 | if 'shortsleep' in sys.argv:
39 | time.sleep(0.1)
40 | txt.append("Shortsleep completed")
41 |
42 | if 'longsleep' in sys.argv:
43 | time.sleep(10)
44 | txt.append("Longsleep completed")
45 |
46 | if __name__ == '__main__':
47 | if len(txt) == 0:
48 | txt.append('Nothing passed')
49 | ec = EC_NOARGS
50 | print("\n".join(txt))
51 | sys.exit(ec)
52 |
--------------------------------------------------------------------------------
/lib/vsc/utils/wrapper.py:
--------------------------------------------------------------------------------
1 | ### External compatible license
2 | """
3 | This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License
4 | with attribution required
5 |
6 | Original code by http://stackoverflow.com/users/416467/kindall from answer 4 of
7 | http://stackoverflow.com/questions/9057669/how-can-i-intercept-calls-to-pythons-magic-methods-in-new-style-classes
8 | """
9 |
10 |
11 | class WrapperMetaclass(type):
12 | def __init__(cls, name, bases, dct):
13 | def make_proxy(name):
14 | def proxy(self, *args): # pylint:disable=unused-argument
15 | return getattr(self._obj, name)
16 |
17 | return proxy
18 |
19 | type.__init__(cls, name, bases, dct)
20 | if cls.__wraps__:
21 | ignore = {f"__{n}__" for n in cls.__ignore__.split()}
22 | for name in dir(cls.__wraps__):
23 | if name.startswith("__"):
24 | if name not in ignore and name not in dct:
25 | setattr(cls, name, property(make_proxy(name)))
26 |
27 |
28 | class Wrapper(metaclass=WrapperMetaclass):
29 | """Wrapper class that provides proxy access to an instance of some
30 | internal instance."""
31 |
32 | __wraps__ = None
33 | __ignore__ = "class mro new init setattr getattr getattribute"
34 |
35 | def __init__(self, obj):
36 | if self.__wraps__ is None:
37 | raise TypeError("base class Wrapper may not be instantiated")
38 | elif isinstance(obj, self.__wraps__):
39 | self._obj = obj
40 | else:
41 | raise ValueError(f"wrapped object must be of {self.__wraps__}")
42 |
43 | # provide proxy access to regular attributes of wrapped object
44 | def __getattr__(self, name):
45 | return getattr(self._obj, name)
46 |
--------------------------------------------------------------------------------
/.github/workflows/unittest.yml:
--------------------------------------------------------------------------------
1 | # .github/workflows/unittest.yml: configuration file for github actions worflow
2 | # This file was automatically generated using 'python -m vsc.install.ci'
3 | # DO NOT EDIT MANUALLY
4 | jobs:
5 | python_ruff_check:
6 | runs-on: ubuntu-24.04
7 | steps:
8 | - name: Checkout code
9 | uses: actions/checkout@v4
10 | - name: Setup Python
11 | uses: actions/setup-python@v5
12 | with:
13 | python-version: ${{matrix.python}}
14 | - name: install ruff
15 | run: pip install 'ruff'
16 | - name: Run ruff
17 | run: ruff check .
18 | strategy:
19 | matrix:
20 | python:
21 | - 3.9
22 | python_ruff_format:
23 | runs-on: ubuntu-24.04
24 | steps:
25 | - name: Checkout code
26 | uses: actions/checkout@v4
27 | - name: Setup Python
28 | uses: actions/setup-python@v5
29 | with:
30 | python-version: ${{matrix.python}}
31 | - name: install ruff
32 | run: pip install 'ruff'
33 | - name: Run ruff format
34 | run: ruff format --check .
35 | strategy:
36 | matrix:
37 | python:
38 | - 3.9
39 | python_unittests:
40 | runs-on: ubuntu-24.04
41 | steps:
42 | - name: Checkout code
43 | uses: actions/checkout@v4
44 | - name: Setup Python
45 | uses: actions/setup-python@v5
46 | with:
47 | python-version: ${{ matrix.python }}
48 | - name: install tox
49 | run: pip install 'virtualenv' 'tox'
50 | - name: add mandatory git remote
51 | run: git remote add hpcugent https://github.com/hpcugent/vsc-base.git
52 | - name: Run tox
53 | run: tox -e py$(echo ${{ matrix.python }} | sed 's/\.//g')
54 | strategy:
55 | matrix:
56 | python:
57 | - 3.9
58 | name: run python tests
59 | 'on':
60 | - push
61 | - pull_request
62 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright 2009-2025 Ghent University
4 | #
5 | # This file is part of vsc-base,
6 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
7 | # with support of Ghent University (http://ugent.be/hpc),
8 | # the Flemish Supercomputer Centre (VSC) (https://vscentrum.be/nl/en),
9 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
10 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
11 | #
12 | # http://github.com/hpcugent/vsc-base
13 | #
14 | # vsc-base is free software: you can redistribute it and/or modify
15 | # it under the terms of the GNU Library General Public License as
16 | # published by the Free Software Foundation, either version 2 of
17 | # the License, or (at your option) any later version.
18 | #
19 | # vsc-base is distributed in the hope that it will be useful,
20 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 | # GNU Library General Public License for more details.
23 | #
24 | # You should have received a copy of the GNU Library General Public License
25 | # along with vsc-base. If not, see .
26 | #
27 | """
28 | vsc-base base distribution setup.py
29 |
30 | @author: Stijn De Weirdt (Ghent University)
31 | @author: Andy Georges (Ghent University)
32 | @author: Kenneth Hoste (Ghent University)
33 | """
34 |
35 | from vsc.install import shared_setup
36 | from vsc.install.shared_setup import ag, kh, jt, sdw, wdp
37 |
38 | PACKAGE = {
39 | "version": "3.6.8",
40 | "author": [sdw, jt, ag, kh],
41 | "maintainer": [sdw, jt, ag, kh, wdp],
42 | "install_requires": [
43 | "vsc-install >= 0.17.19",
44 | ],
45 | }
46 |
47 | if __name__ == "__main__":
48 | shared_setup.action_target(PACKAGE)
49 |
--------------------------------------------------------------------------------
/test/runtests/simple_option.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2011-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | An example how simple_option can do its magic.
28 |
29 | Run it with -h and/or -H to see the help functions.
30 |
31 | To see it do something, try
32 | python examples/simple_option.py --info -L itjustworks
33 |
34 | @author: Stijn De Weirdt (Ghent University)
35 | """
36 | from vsc.utils.generaloption import simple_option
37 |
38 | # dict = {longopt:(help_description,type,action,default_value,shortopt),}
39 | options = {'long1':('1st long option', None, 'store', 'excellent', 'L')}
40 |
41 | go = simple_option(options)
42 |
43 | go.log.info("1st option %s" % go.options.long1)
44 | go.log.debug("DEBUG 1st option %s" % go.options.long1)
45 |
46 |
--------------------------------------------------------------------------------
/examples/simple_option.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright 2011-2013 Ghent University
4 | #
5 | # This file is part of vsc-base,
6 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
7 | # with support of Ghent University (http://ugent.be/hpc),
8 | # the Flemish Supercomputer Centre (VSC) (https://vscentrum.be/nl/en),
9 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
10 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
11 | #
12 | # http://github.com/hpcugent/vsc-base
13 | #
14 | # vsc-base is free software: you can redistribute it and/or modify
15 | # it under the terms of the GNU Library General Public License as
16 | # published by the Free Software Foundation, either version 2 of
17 | # the License, or (at your option) any later version.
18 | #
19 | # vsc-base is distributed in the hope that it will be useful,
20 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 | # GNU Library General Public License for more details.
23 | #
24 | # You should have received a copy of the GNU Library General Public License
25 | # along with vsc-base. If not, see .
26 | #
27 | """
28 | An example how simple_option can do its magic.
29 |
30 | Run it with -h and/or -H to see the help functions.
31 |
32 | To see it do something, try
33 | python examples/simple_option.py --info -L itjustworks
34 |
35 | @author: Stijn De Weirdt (Ghent University)
36 | """
37 |
38 | import logging
39 | from vsc.utils.generaloption import simple_option
40 |
41 | # dict = {longopt:(help_description,type,action,default_value,shortopt),}
42 | options = {"long1": ("1st long option", None, "store", "excellent", "L")}
43 |
44 | go = simple_option(options)
45 |
46 | go.log.info("1st option %s", go.options.long1)
47 | go.log.debug("DEBUG 1st option %s", go.options.long1)
48 |
49 | logging.info("logging from logging.info (eg from 3rd party module)")
50 | logging.debug("logging with logging.root root %s", logging.root)
51 |
--------------------------------------------------------------------------------
/lib/vsc/utils/patterns.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2012-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Module offering the Singleton class.
28 |
29 |
30 | This class can be used as the C{__metaclass__} class field to ensure only a
31 | single instance of the class gets used in the run of an application or
32 | script.
33 |
34 | >>> class A(object):
35 | ... __metaclass__ = Singleton
36 |
37 | @author: Andy Georges (Ghent University)
38 | """
39 |
40 | from abc import ABCMeta
41 |
42 |
43 | class Singleton(ABCMeta):
44 | """Serves as metaclass for classes that should implement the Singleton pattern.
45 |
46 | See http://stackoverflow.com/questions/6760685/creating-a-singleton-in-python
47 | """
48 |
49 | _instances = {}
50 |
51 | def __call__(cls, *args, **kwargs):
52 | if cls not in cls._instances:
53 | cls._instances[cls] = super().__call__(*args, **kwargs)
54 | return cls._instances[cls]
55 |
--------------------------------------------------------------------------------
/test/wrapper.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2014-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Tests for the vsc.utils.wrapper module.
28 |
29 | @author: Stijn De Weirdt (Ghent University)
30 | """
31 | from vsc.install.testing import TestCase
32 |
33 | from vsc.utils.wrapper import Wrapper
34 |
35 |
36 | class TestWrapper(TestCase):
37 | """Test for the Wrapper class."""
38 | def test_wrapper(self):
39 | """Use the tests provided by the stackoverflow page"""
40 | class DictWrapper(Wrapper):
41 | __wraps__ = dict
42 |
43 | wrapped_dict = DictWrapper(dict(a=1, b=2, c=3))
44 |
45 | self.assertTrue("b" in wrapped_dict) # __contains__
46 | self.assertEqual(wrapped_dict, dict(a=1, b=2, c=3)) # __eq__
47 | self.assertTrue("'a': 1" in str(wrapped_dict)) # __str__
48 | self.assertTrue(wrapped_dict.__doc__.startswith("dict()")) # __doc__
49 |
50 |
51 | def suite():
52 | """ return all the tests"""
53 | return TestLoader().loadTestsFromTestCase(TestWrapper)
54 |
--------------------------------------------------------------------------------
/test/docs.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2015-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Unit tests for the docs module.
28 |
29 | @author: Kenneth Hoste (Ghent University)
30 | @author: Caroline De Brouwer (Ghent University)
31 | """
32 | import os
33 | from vsc.install.testing import TestCase
34 |
35 | from vsc.utils.docs import mk_rst_table
36 |
37 |
38 | class DocsTest(TestCase):
39 | """Tests for docs functions."""
40 |
41 | def test_mk_rst_table(self):
42 | """Test mk_rst_table function."""
43 | entries = [['one', 'two', 'three']]
44 | t = 'This title is longer than the entries in the column'
45 | titles = [t]
46 |
47 | # small table
48 | table = mk_rst_table(titles, entries)
49 | check = [
50 | '=' * len(t),
51 | t,
52 | '=' * len(t),
53 | 'one' + ' ' * (len(t) - 3),
54 | 'two' + ' ' * (len(t) -3),
55 | 'three' + ' ' * (len(t) - 5),
56 | '=' * len(t),
57 | '',
58 | ]
59 | self.assertEqual(table, check)
60 |
61 | def suite():
62 | """ returns all the testcases in this module """
63 | return TestLoader().loadTestsFromTestCase(DocsTest)
64 |
65 | if __name__ == '__main__':
66 | main()
67 |
--------------------------------------------------------------------------------
/lib/vsc/utils/frozendict.py:
--------------------------------------------------------------------------------
1 | ### External compatible license
2 | # taken from https://github.com/slezica/python-frozendict on March 14th 2014 (commit ID b27053e4d1)
3 | #
4 | # Copyright (c) 2012 Santiago Lezica
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
7 | # documentation files (the "Software"), to deal in the Software without restriction, including without limitation
8 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
9 | # and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in all copies or substantial portions
12 | # of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
15 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
17 | # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
18 | # IN THE SOFTWARE
19 | """
20 | frozendict is an immutable wrapper around dictionaries that implements the complete mapping interface.
21 | It can be used as a drop-in replacement for dictionaries where immutability is desired.
22 | """
23 |
24 | import operator
25 | from functools import reduce
26 | from collections.abc import Mapping
27 |
28 |
29 | class FrozenDict(Mapping):
30 | def __init__(self, *args, **kwargs):
31 | self.__dict = dict(*args, **kwargs)
32 | self.__hash = None
33 |
34 | def __getitem__(self, key):
35 | return self.__dict[key]
36 |
37 | def copy(self, **add_or_replace):
38 | return FrozenDict(self, **add_or_replace)
39 |
40 | def __iter__(self):
41 | return iter(self.__dict)
42 |
43 | def __len__(self):
44 | return len(self.__dict)
45 |
46 | def __repr__(self):
47 | return f""
48 |
49 | def __hash__(self):
50 | if self.__hash is None:
51 | self.__hash = reduce(operator.xor, map(hash, self.items()), 0)
52 |
53 | return self.__hash
54 |
55 | # minor adjustment: define missing keys() method
56 | def keys(self):
57 | return self.__dict.keys()
58 |
--------------------------------------------------------------------------------
/test/groups.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2012-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | tests for groups module
28 | """
29 | import pwd
30 | import logging
31 |
32 | # Uncomment when debugging, cannot enable permanetnly, messes up tests that toggle debugging
33 | #logging.basicConfig(level=logging.DEBUG)
34 |
35 | from vsc.install.testing import TestCase
36 |
37 | from vsc.utils.groups import getgrouplist
38 | from vsc.utils.run import run
39 |
40 | class GroupsTest(TestCase):
41 | """TestCase for groups"""
42 |
43 | def test_getgrouplist(self):
44 | """Test getgrouplist"""
45 | for user in pwd.getpwall():
46 | gidgroups = sorted(getgrouplist(user.pw_uid))
47 | namegroups = sorted(getgrouplist(user.pw_name))
48 |
49 | # get named groups from id
50 | # grp.getgrall is wrong, e.g. for root user on F30,
51 | # gr_mem for root group is empty (as is entry in /etc/group),
52 | # while id returns 1 group
53 | #groups = [g.gr_name for g in grp.getgrall() if user.pw_name in g.gr_mem]
54 | ec, groups_txt = run(['id', '-Gn', user.pw_name])
55 | groups = sorted(groups_txt.strip().split())
56 |
57 | logging.debug("User %s gidgroups %s namegroups %s groups %s",
58 | user, gidgroups, namegroups, groups)
59 | self.assertEqual(gidgroups, namegroups)
60 | self.assertEqual(gidgroups, groups)
61 |
--------------------------------------------------------------------------------
/test/asyncprocess.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2012-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Unit tests for asyncprocess.py.
28 |
29 | @author: Toon Willems (Ghent University)
30 | @author: Stijn De Weirdt (Ghent University)
31 | """
32 |
33 | import os
34 | import time
35 | from vsc.install.testing import TestCase
36 |
37 | import vsc.utils.asyncprocess as p
38 | from vsc.utils.asyncprocess import Popen
39 |
40 |
41 | def p_recv_some_exception(*args, **kwargs):
42 | """Call recv_some with raise exception enabled"""
43 | kwargs['e'] = True
44 | return p.recv_some(*args, **kwargs)
45 |
46 |
47 | class AsyncProcessTest(TestCase):
48 | """ Testcase for asyncprocess """
49 |
50 | def setUp(self):
51 | """ setup a basic shell """
52 | self.shell = Popen('sh', stdin=p.PIPE, stdout=p.PIPE, shell=True, executable='/bin/bash')
53 | self.cwd = os.getcwd()
54 | super().setUp()
55 |
56 | def runTest(self):
57 | """ try echoing some text and see if it comes back out """
58 | p.send_all(self.shell, "echo hello\n")
59 | time.sleep(0.1)
60 | self.assertEqual(p.recv_some(self.shell), b"hello\n")
61 |
62 | p.send_all(self.shell, "echo hello world\n")
63 | time.sleep(0.1)
64 | self.assertEqual(p.recv_some(self.shell), b"hello world\n")
65 |
66 | p.send_all(self.shell, "exit\n")
67 | time.sleep(0.2)
68 | self.assertEqual(b"", p.recv_some(self.shell, e=False))
69 | self.assertRaises(Exception, p_recv_some_exception, self.shell)
70 |
71 | def tearDown(self):
72 | """cleanup"""
73 | os.chdir(self.cwd)
74 |
--------------------------------------------------------------------------------
/bin/optcomplete.bash:
--------------------------------------------------------------------------------
1 | #******************************************************************************\
2 | # * Copyright (c) 2003-2004, Martin Blais
3 | # * All rights reserved.
4 | # *
5 | # * Redistribution and use in source and binary forms, with or without
6 | # * modification, are permitted provided that the following conditions are
7 | # * met:
8 | # *
9 | # * * Redistributions of source code must retain the above copyright
10 | # * notice, this list of conditions and the following disclaimer.
11 | # *
12 | # * * Redistributions in binary form must reproduce the above copyright
13 | # * notice, this list of conditions and the following disclaimer in the
14 | # * documentation and/or other materials provided with the distribution.
15 | # *
16 | # * * Neither the name of the Martin Blais, Furius, nor the names of its
17 | # * contributors may be used to endorse or promote products derived from
18 | # * this software without specific prior written permission.
19 | # *
20 | # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 | # * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 | # * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 | # * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24 | # * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 | # * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 | # * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 | # * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 | # * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 | # * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | # * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 | #******************************************************************************\
32 | #
33 | # stdweird: This is a copy of etc/optcomplete.bash (changeset 17:e0a9131a94cc)
34 | # stdweird: source: https://hg.furius.ca/public/optcomplete
35 | #
36 | # optcomplete harness for bash shell. You then need to tell
37 | # bash to invoke this shell function with a command like
38 | # this:
39 | #
40 | # complete -F _optcomplete
41 | #
42 |
43 | _optcomplete()
44 | {
45 | # needed to let it return _filedir based commands
46 | local cur prev quoted
47 | _get_comp_words_by_ref cur prev
48 | _quote_readline_by_ref "$cur" quoted
49 | _expand || return 0
50 |
51 | # call the command with the completion information, then eval it's results so it can call _filedir or similar
52 | # this does have the potential to be a security problem, especially if running as root, but we should trust
53 | # the completions as we're planning to run this script anyway
54 | eval $( \
55 | COMP_LINE=$COMP_LINE COMP_POINT=$COMP_POINT \
56 | COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD \
57 | OPTPARSE_AUTO_COMPLETE=1 $1
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/lib/vsc/utils/docs.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2015-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Functions for generating rst documentation
28 |
29 | @author: Caroline De Brouwer (Ghent University)
30 | """
31 |
32 |
33 | class LengthNotEqualException(ValueError):
34 | pass
35 |
36 |
37 | def mk_rst_table(titles, columns):
38 | """
39 | Returns an rst table with given titles and columns (a nested list of string columns for each column)
40 | """
41 | # make sure that both titles and columns are actual lists (not generator objects),
42 | # since we use them multiple times (and a generator can only be consumed once
43 | titles, columns = list(titles), list(columns)
44 |
45 | title_cnt, col_cnt = len(titles), len(columns)
46 | if title_cnt != col_cnt:
47 | msg = f"Number of titles/columns should be equal, found {int(title_cnt)} titles and {int(col_cnt)} columns"
48 | raise LengthNotEqualException(msg)
49 |
50 | table = []
51 | separator_blocks = []
52 | title_items = []
53 | column_widths = []
54 |
55 | for i, title in enumerate(titles):
56 | # figure out column widths
57 | width = max(map(len, columns[i] + [title]))
58 |
59 | column_widths.append(width)
60 | separator_blocks.append(f"{'=' * width}")
61 | title_items.append(f"{title}".ljust(width))
62 |
63 | separator_line = " ".join(separator_blocks)
64 |
65 | # header
66 | table.extend([separator_line, " ".join(title_items), separator_line])
67 |
68 | # rows
69 | for row in map(list, zip(*columns)):
70 | row_items = []
71 | for i, item in enumerate(row):
72 | row_items.append(item.ljust(column_widths[i]))
73 | table.append(" ".join(row_items))
74 |
75 | # footer
76 | table.extend([separator_line, ""])
77 |
78 | return table
79 |
--------------------------------------------------------------------------------
/examples/qa.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright 2013-2013 Ghent University
4 | #
5 | # This file is part of vsc-base,
6 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
7 | # with support of Ghent University (http://ugent.be/hpc),
8 | # the Flemish Supercomputer Centre (VSC) (https://vscentrum.be/nl/en),
9 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
10 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
11 | #
12 | # http://github.com/hpcugent/vsc-base
13 | #
14 | # vsc-base is free software: you can redistribute it and/or modify
15 | # it under the terms of the GNU Library General Public License as
16 | # published by the Free Software Foundation, either version 2 of
17 | # the License, or (at your option) any later version.
18 | #
19 | # vsc-base is distributed in the hope that it will be useful,
20 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 | # GNU Library General Public License for more details.
23 | #
24 | # You should have received a copy of the GNU Library General Public License
25 | # along with vsc-base. If not, see .
26 | #
27 | """
28 | Simple QA script
29 |
30 | @author: Stijn De Weirdt (Ghent University)
31 | """
32 |
33 | import os
34 |
35 | from vsc.utils.run import run_qa, run_qalog, run_qastdout, run_async_to_stdout
36 | from vsc.utils.generaloption import simple_option
37 |
38 | go = simple_option(None)
39 |
40 | SCRIPT_DIR = os.path.join(os.path.dirname(__file__), "..", "test", "runtests")
41 | SCRIPT_QA = os.path.join(SCRIPT_DIR, "qa.py")
42 |
43 |
44 | def test_qa():
45 | qa_dict = {
46 | "Simple question:": "simple answer",
47 | }
48 | ec, output = run_qa([SCRIPT_QA, "simple"], qa=qa_dict)
49 | return ec, output
50 |
51 |
52 | def test_qalog():
53 | qa_dict = {
54 | "Simple question:": "simple answer",
55 | }
56 | ec, output = run_qalog([SCRIPT_QA, "simple"], qa=qa_dict)
57 | return ec, output
58 |
59 |
60 | def test_qastdout():
61 | run_async_to_stdout([SCRIPT_QA, "simple"])
62 | qa_dict = {
63 | "Simple question:": "simple answer",
64 | }
65 | ec, output = run_qastdout([SCRIPT_QA, "simple"], qa=qa_dict)
66 | return ec, output
67 |
68 |
69 | def test_std_regex():
70 | qa_dict = {
71 | r"\s(?P\d+(?:\.\d+)?)\..*?What time is it\?": "%(time)s",
72 | }
73 | ec, output = run_qastdout([SCRIPT_QA, "whattime"], qa_reg=qa_dict)
74 | return ec, output
75 |
76 |
77 | def test_qa_noqa():
78 | qa_dict = {
79 | "Now is the time.": "OK",
80 | }
81 | no_qa = ["Wait for it \(\d+ seconds\)"]
82 | ec, output = run_qastdout([SCRIPT_QA, "waitforit"], qa=qa_dict, no_qa=no_qa)
83 | return ec, output
84 |
85 |
86 | def test_qanoquestion():
87 | ec, output = run_qalog([SCRIPT_QA, "noquestion"])
88 | return ec, output
89 |
90 |
91 | if __name__ == "__main__":
92 | test_qanoquestion()
93 | test_qastdout()
94 | test_qa()
95 | test_qalog()
96 | test_std_regex()
97 | test_qa_noqa()
98 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vsc-base
2 |
3 | # Description
4 |
5 | Common tools used within our organization.
6 | Originally created by the HPC team of Ghent University (https://ugent.be/hpc).
7 |
8 | # Namespaces and tools
9 |
10 | ## lib/utils
11 | python utilities to be used as libraries
12 |
13 | - __fancylogger__: an extention of the default python logger designed to be easy to use and have a couple of `fancy` features.
14 |
15 | - custom specifiers for mpi loggin (the mpirank) with autodetection of mpi
16 | - custom specifier for always showing the calling function's name
17 | - rotating file handler
18 | - a default formatter.
19 | - logging to an UDP server (logdaemon.py f.ex.)
20 | - easily setting loglevel
21 |
22 | - __daemon.py__ : Daemon class written by Sander Marechal (http://www.jejik.com) to start a python script as a daemon.
23 | - __missing.py__: Small functions and tools that are commonly used but not available in the Python (2.x) API.
24 | - ~~__cache.py__ : File cache to store pickled data identified by a key accompanied by a timestamp.~~ (moved to [vsc-utils](https://github.com/hpcugent/vsc-utils))
25 | - __generaloption.py__ : A general option parser for python. It will fetch options (in this order) from config files, from environment variables and from the command line and parse them in a way compatible with the default python optionparser. Thus allowing a very flexible way to configure your scripts. It also adds a few other useful extras.
26 | - __affinity.py__ : Linux cpu affinity.
27 |
28 | - Based on `sched.h` and `bits/sched.h`,
29 | - see man pages for `sched_getaffinity` and `sched_setaffinity`
30 | - also provides a `cpuset` class to convert between human readable cpusets and the bit version Linux priority
31 | - Based on sys/resources.h and bits/resources.h see man pages for `getpriority` and `setpriority`
32 |
33 | - __asyncprocess.py__ : Module to allow Asynchronous subprocess use on Windows and Posix platforms
34 |
35 | - Based on a [python recipe](http://code.activestate.com/recipes/440554/) by Josiah Carlson
36 | - added STDOUT handle and recv_some
37 |
38 | - __daemon.py__ : [A generic daemon class by Sander Marechal](http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/)
39 | - __dateandtime.py__ : A module with various convenience functions and classes to deal with date, time and timezone.
40 | - __nagios.py__ : This module provides functionality to cache and report results of script executions that can readily be interpreted by nagios/icinga.
41 | - __run.py__ : Python module to execute a command, can make use of asyncprocess, answer questions based on a dictionary
42 |
43 | - supports a whole lot of ways to input, process and output the command. (filehandles, PIPE, pty, stdout, logging...)
44 |
45 | - __mail.py__ : Wrapper around the standard Python mail library.
46 |
47 | - Send a plain text message
48 | - Send an HTML message, with a plain text alternative
49 |
50 | # Acknowledgements
51 | vsc-base was created with support of [Ghent University](https://www.ugent.be/en),
52 | the [Flemish Supercomputer Centre (VSC)](https://vscentrum.be/nl/en),
53 | the [Flemish Research Foundation (FWO)](https://www.fwo.be/en),
54 | and [the Department of Economy, Science and Innovation (EWI)](https://www.ewi-vlaanderen.be/en).
55 |
56 |
--------------------------------------------------------------------------------
/lib/vsc/utils/groups.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2018-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | This module contains tools related to users and groups
28 | """
29 |
30 | import grp
31 | import pwd
32 | from ctypes import c_char_p, c_uint, c_int32, POINTER, byref, cdll
33 | from ctypes.util import find_library
34 |
35 |
36 | def getgrouplist(user, groupnames=True):
37 | """
38 | Return a list of all groupid's for groups this user is in
39 | This function is needed here because python's user database only contains local users, not remote users from e.g.
40 | sssd
41 | user can be either an integer (uid) or a string (username)
42 | returns a list of groupnames
43 | if groupames is false, returns a list of groupids (skip groupname lookups)
44 | """
45 | libc = cdll.LoadLibrary(find_library("c"))
46 |
47 | ngetgrouplist = libc.getgrouplist
48 | # max of 50 groups should be enough as first try
49 | ngroups = 50
50 | ngetgrouplist.argtypes = [c_char_p, c_uint, POINTER(c_uint * ngroups), POINTER(c_int32)]
51 | ngetgrouplist.restype = c_int32
52 |
53 | grouplist = (c_uint * ngroups)()
54 | ngrouplist = c_int32(ngroups)
55 |
56 | if isinstance(user, str):
57 | user = pwd.getpwnam(user)
58 | else:
59 | user = pwd.getpwuid(user)
60 |
61 | # .encode() is required in Python 3, since we need to pass a bytestring to getgrouplist
62 | user_name, user_gid = user.pw_name.encode(), user.pw_gid
63 |
64 | ct = ngetgrouplist(user_name, user_gid, byref(grouplist), byref(ngrouplist))
65 | # if a max of 50 groups was not enough, try again with exact given nr
66 | if ct < 0:
67 | ngetgrouplist.argtypes = [c_char_p, c_uint, POINTER(c_uint * int(ngrouplist.value)), POINTER(c_int32)]
68 | grouplist = (c_uint * int(ngrouplist.value))()
69 | ct = ngetgrouplist(user_name, user_gid, byref(grouplist), byref(ngrouplist))
70 |
71 | if ct < 0:
72 | raise ValueError(f"Could not find groups for {user}: getgrouplist returned {ct}")
73 |
74 | grouplist = [grouplist[i] for i in range(ct)]
75 | if groupnames:
76 | grouplist = [grp.getgrgid(i).gr_name for i in grouplist]
77 | return grouplist
78 |
--------------------------------------------------------------------------------
/test/runtests/qa.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2016-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Simple, ugly test script for QA
28 | """
29 | import time
30 | import re
31 | import sys
32 |
33 | TIMEOUT = 5 # should be enough
34 | now = time.time()
35 |
36 | res = {}
37 |
38 | qa = {
39 | 'ask_number': ("Enter a number ('0' to stop):", r'^\d+$'),
40 | 'noquestion': (None, None),
41 | 'simple': ('Simple question: ', 'simple answer'),
42 | 'whattime': ('Now it is %s. What time is it? ' % now, "%s" % now),
43 | 'waitforit': ('Now is the time.', 'OK'),
44 | 'nonewline': ('Do NOT give me a newline', 'Sure'),
45 | }
46 |
47 | for k, v in qa.items():
48 | if k == sys.argv[1]:
49 |
50 | # sanity case: no answer and no question
51 | if v[0] is None:
52 | res[k] = [True, None]
53 | else:
54 | loop_cnt = 1
55 | if k == 'waitforit':
56 | print('Wait for it (%d seconds)' % TIMEOUT, end='')
57 | sys.stdout.flush()
58 | time.sleep(TIMEOUT)
59 | elif k == 'ask_number':
60 | if len(sys.argv) == 3:
61 | loop_cnt = int(sys.argv[2])
62 |
63 | for i in range(0, loop_cnt):
64 | print(v[0], end='')
65 | sys.stdout.flush()
66 |
67 | # for all regular cases, we do not care if there was a newline
68 | # but for the case where no newline is added to the answer, we
69 | # better not strip it :)
70 | if k == 'nonewline':
71 | a = sys.stdin.read(len(v[1]))
72 | else:
73 | a = sys.stdin.readline().rstrip('\n')
74 | a_re = re.compile(v[1])
75 |
76 | if k == 'ask_number':
77 | if a == '0':
78 | # '0' means stop
79 | break
80 | prev = 0
81 | if k in res:
82 | prev = int(res[k][1])
83 | a = str(prev + int(a))
84 |
85 | res[k] = [a_re.match(a), a]
86 |
87 |
88 | if __name__ == '__main__':
89 | failed = 0
90 |
91 | for k, v in res.items():
92 | if k == 'nonewline':
93 | sys.exit(0)
94 | if v[0]:
95 | if k == 'nonewline':
96 | if v[1][-1] == '\n':
97 | failed += 1
98 | print("Test %s NOT OK, did not expect a newline in the answer" % (k,))
99 | sys.exit(57)
100 | else:
101 | sys.exit(0)
102 | if 'ask_number' in k:
103 | print("Answer: %s" % v[1])
104 | else:
105 | print("Test %s OK" % k)
106 | else:
107 | failed += 2
108 | print("Test %s NOT OK expected answer '%s', received '%s'" % (k, qa[k][1], v[1]))
109 |
110 | sys.exit(failed)
111 |
--------------------------------------------------------------------------------
/test/dateandtime.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2012-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Python module for handling data and time strings.
28 |
29 | @author: Stijn De Weirdt (Ghent University)
30 | """
31 |
32 | import datetime
33 | from vsc.install.testing import TestCase
34 | from vsc.utils.dateandtime import FancyMonth, date_parser, datetime_parser
35 |
36 | class DateAndTimeTest(TestCase):
37 | """Tests for dateandtime"""
38 |
39 | def test_date_parser(self):
40 | """Test the date_parser"""
41 | testdate = datetime.date(1970, 1, 1)
42 | self.assertEqual(date_parser('1970-01-01') , testdate)
43 | self.assertEqual(date_parser('1970-1-1'), testdate)
44 |
45 | today = datetime.date.today()
46 | beginthismonth = datetime.date(today.year, today.month, 1)
47 | self.assertEqual(date_parser('BEGINTHISMONTH') , beginthismonth)
48 |
49 | endapril = datetime.date(today.year, 4, 30)
50 | self.assertEqual(date_parser('ENDAPRIL') , endapril)
51 |
52 | todaytime = datetime.datetime(today.year, today.month, today.day)
53 | oneday = datetime.timedelta(days=1)
54 |
55 | self.assertEqual(date_parser('TODAY'), today)
56 | self.assertEqual(date_parser('YESTERDAY'), (todaytime - oneday).date())
57 | self.assertEqual(date_parser('TOMORROW'), (todaytime + oneday).date())
58 |
59 | def test_datetime_parser(self):
60 | """Test the date_parser"""
61 | testdate = datetime.datetime(1970, 1, 1)
62 | self.assertEqual(datetime_parser('1970-01-01') , testdate)
63 | self.assertEqual(datetime_parser('1970-1-1'), testdate)
64 |
65 | today = datetime.datetime.today()
66 | todaytime = datetime.datetime(today.year, today.month, today.day)
67 | oneday = datetime.timedelta(days=1)
68 |
69 | self.assertEqual(datetime_parser('TODAY'), todaytime)
70 | self.assertEqual(datetime_parser('YESTERDAY'), todaytime - oneday)
71 | self.assertEqual(datetime_parser('TOMORROW'), todaytime + oneday)
72 |
73 | beginthismonth = datetime.datetime(today.year, today.month, 1, 12, 1)
74 | self.assertEqual(datetime_parser('BEGINTHISMONTH 12:1') , beginthismonth)
75 |
76 | todayend = todaytime
77 | self.assertEqual(datetime_parser('TODAY BEGIN'), todaytime)
78 | self.assertEqual(datetime_parser('TODAY END'), todaytime + oneday - datetime.timedelta(seconds=1))
79 | self.assertEqual(datetime_parser('YESTERDAY END'), todaytime - datetime.timedelta(seconds=1))
80 |
81 | def test_fancymonth(self):
82 | """Test some of the FancyMonth functions"""
83 | today = datetime.date.today()
84 | endapril = datetime.date(today.year, 4, 10)
85 | f = FancyMonth(endapril)
86 | self.assertEqual(f.nrdays, 30) # nr days in month
87 |
88 | may2nd = datetime.date(today.year, 5, 2)
89 | self.assertEqual(f.number(may2nd), 2) # spans 2 months
90 | self.assertEqual([x.nrdays for x in f.interval(may2nd)], [30, 31]) # interval returns FancyMonth instances
91 |
92 |
93 | def suite():
94 | """ returns all the testcases in this module """
95 | return TestLoader().loadTestsFromTestCase(DateAndTimeTest)
96 |
--------------------------------------------------------------------------------
/lib/vsc/utils/daemon.py:
--------------------------------------------------------------------------------
1 | ### External compatible license
2 | #
3 | # Copyright 2007 Sander Marechal (http://www.jejik.com)
4 | # Released as Public Domain
5 | # Retrieved from:
6 | # http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/
7 | #
8 | """
9 | Module to make python scripts run in background.
10 |
11 | @author: Sander Marechal
12 | """
13 |
14 | import atexit
15 | import logging
16 | import os
17 | import sys
18 | import time
19 | from signal import SIGTERM
20 |
21 |
22 | class Daemon:
23 | """
24 | A generic daemon class.
25 |
26 | Usage: subclass the Daemon class and override the run() method
27 | """
28 |
29 | def __init__(self, pidfile, stdin="/dev/null", stdout="/dev/null", stderr="/dev/null"):
30 | self.stdin = stdin
31 | self.stdout = stdout
32 | self.stderr = stderr
33 | self.pidfile = pidfile
34 |
35 | def daemonize(self):
36 | """
37 | do the UNIX double-fork magic, see Stevens' "Advanced
38 | Programming in the UNIX Environment" for details (ISBN 0201563177)
39 | http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
40 | """
41 | try:
42 | pid = os.fork()
43 | if pid > 0:
44 | # exit first parent
45 | sys.exit(0)
46 | except OSError as err:
47 | sys.stderr.write(f"fork #1 failed: {int(err.errno)} ({err.strerror})\n")
48 | sys.exit(1)
49 |
50 | # decouple from parent environment
51 | os.chdir("/")
52 | os.setsid()
53 | os.umask(0)
54 |
55 | # do second fork
56 | try:
57 | pid = os.fork()
58 | if pid > 0:
59 | # exit from second parent
60 | sys.exit(0)
61 | except OSError as err:
62 | sys.stderr.write(f"fork #2 failed: {int(err.errno)} ({err.strerror})\n")
63 | sys.exit(1)
64 |
65 | # redirect standard file descriptors
66 | sys.stdout.flush()
67 | sys.stderr.flush()
68 | with open(self.stdin) as sti:
69 | os.dup2(sti.fileno(), sys.stdin.fileno())
70 | with open(self.stdout, "a+") as sto:
71 | os.dup2(sto.fileno(), sys.stdout.fileno())
72 | with open(self.stderr, "ba+", 0) as ste:
73 | os.dup2(ste.fileno(), sys.stderr.fileno())
74 |
75 | # write pidfile
76 | atexit.register(self.delpid)
77 | pid = str(os.getpid())
78 | with open(self.pidfile, "w+") as pidf:
79 | pidf.write(f"{pid}\n")
80 |
81 | def delpid(self):
82 | os.remove(self.pidfile)
83 |
84 | def start(self):
85 | """
86 | Start the daemon
87 | """
88 | # Check for a pidfile to see if the daemon already runs
89 | try:
90 | with open(self.pidfile) as pidf:
91 | pid = int(pidf.read().strip())
92 | except OSError:
93 | pid = None
94 |
95 | if pid:
96 | message = "pidfile %s already exist. Daemon already running?\n"
97 | sys.stderr.write(message % self.pidfile)
98 | sys.exit(1)
99 |
100 | # Start the daemon
101 | self.daemonize()
102 | self.run()
103 |
104 | def stop(self):
105 | """
106 | Stop the daemon
107 | """
108 | # Get the pid from the pidfile
109 | try:
110 | with open(self.pidfile) as pidf:
111 | pid = int(pidf.read().strip())
112 | except OSError:
113 | pid = None
114 |
115 | if not pid:
116 | message = "pidfile %s does not exist. Daemon not running?\n"
117 | sys.stderr.write(message % self.pidfile)
118 | return # not an error in a restart
119 |
120 | # Try killing the daemon process
121 | try:
122 | while 1:
123 | os.kill(pid, SIGTERM)
124 | time.sleep(0.1)
125 | except OSError as err:
126 | err = str(err)
127 | if err.find("No such process") > 0:
128 | if os.path.exists(self.pidfile):
129 | os.remove(self.pidfile)
130 | else:
131 | logging.error(str(err))
132 | sys.exit(1)
133 |
134 | def restart(self):
135 | """
136 | Restart the daemon
137 | """
138 | self.stop()
139 | self.start()
140 |
141 | def run(self):
142 | """
143 | You should override this method when you subclass Daemon. It will be called after the process has been
144 | daemonized by start() or restart().
145 | """
146 |
--------------------------------------------------------------------------------
/test/mail.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2014-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Unit tests for the mail wrapper.
28 |
29 | @author: Andy Georges (Ghent University)
30 | """
31 | import logging
32 | import os
33 |
34 | from unittest import mock
35 | from vsc.install.testing import TestCase
36 |
37 | from email.mime.text import MIMEText
38 | from vsc.utils.mail import VscMail
39 |
40 | class TestVscMail(TestCase):
41 |
42 |
43 | def test_config_file(self):
44 |
45 | mail_host = "mailhost.domain"
46 | mail_port = 123
47 | mail_host_port = "mailhost.domain:567"
48 | smtp_auth_user = "user"
49 | smtp_auth_password = "passwd"
50 | smtp_use_starttls = True
51 |
52 | mail = VscMail(mail_host=mail_host)
53 |
54 | self.assertEqual(mail.mail_host, mail_host)
55 | self.assertEqual(mail.mail_port, 587)
56 |
57 | mail = VscMail(mail_host=mail_host, mail_port=mail_port)
58 | self.assertEqual(mail.mail_host, mail_host)
59 | self.assertEqual(mail.mail_port, mail_port)
60 |
61 | mail = VscMail(mail_config=os.path.dirname(__file__) + '/data/' + 'mailconfig.ini')
62 |
63 | logging.warning("mail.mail_host: %s", mail.mail_host)
64 |
65 | self.assertEqual(mail.mail_host, "config_host")
66 | self.assertEqual(mail.mail_port, 789)
67 | self.assertEqual(mail.smtp_auth_user, "config_user")
68 | self.assertEqual(mail.smtp_auth_password, "config_passwd")
69 | self.assertEqual(mail.smtp_use_starttls, '1')
70 |
71 | @mock.patch('vsc.utils.mail.smtplib')
72 | @mock.patch('vsc.utils.mail.ssl')
73 | def test_send(self, mock_ssl, mock_smtplib):
74 |
75 | msg = MIMEText("test")
76 | msg['Subject'] = "subject"
77 | msg['From'] = "test@noreply.com"
78 | msg['To'] = "test@noreply.com"
79 | msg['Reply-to'] = "test@noreply.com"
80 |
81 | vm = VscMail()
82 |
83 | self.assertEqual(vm.mail_host, '')
84 | self.assertEqual(vm.mail_port, 587)
85 | self.assertEqual(vm.smtp_auth_user, None)
86 | self.assertEqual(vm.smtp_auth_password, None)
87 | self.assertEqual(vm.smtp_use_starttls, False)
88 |
89 | vm._send(mail_from="test@noreply.com", mail_to="test@noreply.com", mail_subject="s", msg=msg)
90 |
91 | vm = VscMail(
92 | mail_host = "test.machine.com",
93 | mail_port=123,
94 | smtp_auth_user="me",
95 | smtp_auth_password="hunter2",
96 | )
97 |
98 | self.assertEqual(vm.mail_host, "test.machine.com")
99 | self.assertEqual(vm.mail_port, 123)
100 | self.assertEqual(vm.smtp_auth_user, "me")
101 | self.assertEqual(vm.smtp_auth_password, "hunter2")
102 | self.assertEqual(vm.smtp_use_starttls, False)
103 |
104 | vm._send(mail_from="test@noreply.com", mail_to="test@noreply.com", mail_subject="s", msg=msg)
105 |
106 | mock_smtplib.SMTP.assert_called_with(host="test.machine.com", port=123)
107 |
108 | vm = VscMail(
109 | mail_host = "test.machine.com",
110 | mail_port=124,
111 | smtp_auth_user="me",
112 | smtp_auth_password="hunter2",
113 | smtp_use_starttls=True
114 | )
115 |
116 | self.assertEqual(vm.mail_host, "test.machine.com")
117 | self.assertEqual(vm.mail_port, 124)
118 | self.assertEqual(vm.smtp_auth_user, "me")
119 | self.assertEqual(vm.smtp_auth_password, "hunter2")
120 | self.assertEqual(vm.smtp_use_starttls, True)
121 |
122 | vm._send(mail_from="test@noreply.com", mail_to="test@noreply.com", mail_subject="s", msg=msg)
123 |
124 | mock_smtplib.SMTP.assert_called_with(host="test.machine.com", port=124)
125 | mock_ssl.create_default_context.assert_called()
126 |
127 | # test sending a unicode message
128 | vm = VscMail()
129 | vm.sendTextMail(
130 | "test@noreply.com",
131 | "test@noreply.com",
132 | "test@noreply.com",
133 | "subject",
134 | " Καλημέρα κόσμε, コンニチハ",
135 | )
136 |
--------------------------------------------------------------------------------
/lib/vsc/utils/exceptions.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2015-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Module providing custom exceptions.
28 |
29 | @author: Kenneth Hoste (Ghent University)
30 | @author: Riccardo Murri (University of Zurich)
31 | """
32 |
33 | import inspect
34 | import logging
35 | import os
36 | from vsc.utils import fancylogger
37 |
38 |
39 | def get_callers_logger():
40 | """
41 | Get logger defined in caller's environment
42 | @return: logger instance (or None if none was found)
43 | """
44 | logger_cls = logging.getLoggerClass()
45 | if __debug__:
46 | frame = inspect.currentframe()
47 | else:
48 | frame = None
49 | logger = None
50 |
51 | # frame may be None, see https://docs.python.org/2/library/inspect.html#inspect.currentframe
52 | if frame is not None:
53 | try:
54 | # consider calling stack in reverse order, i.e. most inner frame (closest to caller) first
55 | for frameinfo in inspect.getouterframes(frame)[::-1]:
56 | bindings = inspect.getargvalues(frameinfo[0]).locals
57 | for val in bindings.values():
58 | if isinstance(val, logger_cls):
59 | logger = val
60 | break
61 | finally:
62 | # make very sure that reference to frame object is removed, to avoid reference cycles
63 | # see https://docs.python.org/2/library/inspect.html#the-interpreter-stack
64 | del frame
65 |
66 | return logger
67 |
68 |
69 | class LoggedException(Exception):
70 | """Exception that logs it's message when it is created."""
71 |
72 | # logger module to use (must provide getLogger() function)
73 | LOGGER_MODULE = fancylogger
74 | # name of logging method to use
75 | # must accept an argument of type string, i.e. the log message, and an optional list of formatting arguments
76 | LOGGING_METHOD_NAME = "error"
77 | # list of top-level package names to use to format location info; None implies not to include location info
78 | LOC_INFO_TOP_PKG_NAMES = []
79 | # include location where error was raised from (enabled by default under 'python', disabled under 'python -O')
80 | INCLUDE_LOCATION = __debug__
81 |
82 | def __init__(self, msg, *args, **kwargs):
83 | """
84 | Constructor.
85 | @param msg: exception message
86 | @param *args: list of formatting arguments for exception message
87 | @param logger: logger to use
88 | """
89 | # format message with (optional) list of formatting arguments
90 | if args:
91 | msg = msg % args
92 |
93 | if self.LOC_INFO_TOP_PKG_NAMES is not None:
94 | # determine correct frame to fetch location information from
95 | frames_up = 1
96 | if self.__class__ != LoggedException:
97 | # move a level up when this instance is derived from LoggedException
98 | frames_up += 1
99 |
100 | if self.INCLUDE_LOCATION:
101 | # figure out where error was raised from
102 | # current frame: this constructor, one frame above: location where LoggedException was created/raised
103 | frameinfo = inspect.getouterframes(inspect.currentframe())[frames_up]
104 |
105 | # determine short location of Python module where error was raised from,
106 | # i.e. starting with an entry from LOC_INFO_TOP_PKG_NAMES
107 | path_parts = frameinfo[1].split(os.path.sep)
108 | if path_parts[0] == "":
109 | path_parts[0] = os.path.sep
110 | top_indices = [path_parts.index(n) for n in self.LOC_INFO_TOP_PKG_NAMES if n in path_parts]
111 | relpath = os.path.join(*path_parts[max(top_indices or [0]) :])
112 |
113 | # include location info at the end of the message
114 | # for example: "Nope, giving up (at easybuild/tools/somemodule.py:123 in some_function)"
115 | msg = f"{msg} (at {relpath}:{frameinfo[2]} in {frameinfo[3]})"
116 |
117 | logger = kwargs.get("logger", None)
118 | # try to use logger defined in caller's environment
119 | if logger is None:
120 | logger = get_callers_logger()
121 | # search can fail, use root logger as a fallback
122 | if logger is None:
123 | logger = self.LOGGER_MODULE.getLogger()
124 |
125 | getattr(logger, self.LOGGING_METHOD_NAME)(msg)
126 |
127 | super().__init__(msg)
128 |
--------------------------------------------------------------------------------
/lib/vsc/utils/asyncprocess.py:
--------------------------------------------------------------------------------
1 | ### External compatible license
2 | #
3 | # Copyright 2005 Josiah Carlson
4 | # The Asynchronous Python Subprocess recipe was originally created by Josiah Carlson.
5 | # and released under the GNU Library General Public License v2 or any later version
6 | # on Jan 23, 2013.
7 | #
8 | # http://code.activestate.com/recipes/440554/
9 | #
10 | # Copyright 2009-2020 Ghent University
11 | #
12 | # This file is part of vsc-base,
13 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
14 | # with support of Ghent University (http://ugent.be/hpc),
15 | # the Flemish Supercomputer Centre (VSC) (https://vscentrum.be/nl/en),
16 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
17 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
18 | #
19 | # http://github.com/hpcugent/vsc-base
20 | #
21 | # vsc-base is free software: you can redistribute it and/or modify
22 | # it under the terms of the GNU Library General Public License as
23 | # published by the Free Software Foundation, either version 2 of
24 | # the License, or (at your option) any later version.
25 | #
26 | # vsc-base is distributed in the hope that it will be useful,
27 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
28 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 | # GNU Library General Public License for more details.
30 | #
31 | # You should have received a copy of the GNU Library General Public License
32 | # along with vsc-base. If not, see .
33 | #
34 |
35 | """
36 | Module to allow Asynchronous subprocess use on Windows and Posix platforms
37 |
38 | The 'subprocess' module in Python 2.4 has made creating and accessing subprocess
39 | streams in Python relatively convenient for all supported platforms,
40 | but what if you want to interact with the started subprocess?
41 | That is, what if you want to send a command, read the response,
42 | and send a new command based on that response?
43 |
44 | Now there is a solution.
45 | The included subprocess.Popen subclass adds three new commonly used methods:
46 | - C{recv(maxsize=None)}
47 | - C{recv_err(maxsize=None)}
48 | - and C{send(input)}
49 |
50 | along with a utility method:
51 | - {send_recv(input='', maxsize=None)}.
52 |
53 | C{recv()} and C{recv_err()} both read at most C{maxsize} bytes from the started subprocess.
54 | C{send()} sends strings to the started subprocess. C{send_recv()} will send the provided input,
55 | and read up to C{maxsize} bytes from both C{stdout} and C{stderr}.
56 |
57 | If any of the pipes are closed, the attributes for those pipes will be set to None,
58 | and the methods will return None.
59 |
60 | - downloaded 05/08/2010
61 | - modified
62 | - added STDOUT handle
63 | - added maxread to recv_some (2012-08-30)
64 |
65 | @author: Josiah Carlson
66 | @author: Stijn De Weirdt (Ghent University)
67 | """
68 |
69 | import errno
70 | import fcntl # @UnresolvedImport
71 | import os
72 | import select # @UnresolvedImport
73 | import subprocess
74 | import time
75 |
76 | PIPE = subprocess.PIPE
77 | STDOUT = subprocess.STDOUT
78 | MESSAGE = "Other end disconnected!"
79 |
80 |
81 | class Popen(subprocess.Popen):
82 | def recv(self, maxsize=None):
83 | return self._recv("stdout", maxsize)
84 |
85 | def recv_err(self, maxsize=None):
86 | return self._recv("stderr", maxsize)
87 |
88 | def send_recv(self, inp="", maxsize=None):
89 | return self.send(inp), self.recv(maxsize), self.recv_err(maxsize)
90 |
91 | def get_conn_maxsize(self, which, maxsize):
92 | if maxsize is None:
93 | maxsize = 1024
94 | elif maxsize == 0: # do not use < 1: -1 means all
95 | maxsize = 1
96 | return getattr(self, which), maxsize
97 |
98 | def _close(self, which):
99 | getattr(self, which).close()
100 | setattr(self, which, None)
101 |
102 | def send(self, inp):
103 | if not self.stdin:
104 | return None
105 |
106 | if not select.select([], [self.stdin], [], 0)[1]:
107 | return 0
108 |
109 | try:
110 | written = os.write(self.stdin.fileno(), inp)
111 | self.stdin.flush()
112 | except OSError as why:
113 | if why.args[0] == errno.EPIPE: # broken pipe
114 | return self._close("stdin")
115 | raise
116 |
117 | return written
118 |
119 | def _recv(self, which, maxsize):
120 | conn, maxsize = self.get_conn_maxsize(which, maxsize)
121 | if conn is None:
122 | return None
123 |
124 | flags = fcntl.fcntl(conn, fcntl.F_GETFL)
125 | if not conn.closed:
126 | fcntl.fcntl(conn, fcntl.F_SETFL, flags | os.O_NONBLOCK)
127 |
128 | try:
129 | if not select.select([conn], [], [], 0)[0]:
130 | return ""
131 |
132 | r = conn.read(maxsize)
133 | if not r:
134 | return self._close(which) # close when nothing left to read
135 |
136 | if self.universal_newlines:
137 | r = self._translate_newlines(r)
138 | return r
139 | finally:
140 | if not conn.closed:
141 | fcntl.fcntl(conn, fcntl.F_SETFL, flags)
142 |
143 |
144 | def recv_some(p, t=0.1, e=False, tr=5, stderr=False, maxread=None):
145 | """
146 | @param p: process
147 | @param t: max time to wait without any output before returning
148 | @param e: boolean, raise exception is process stopped
149 | @param tr: time resolution used for intermediate sleep
150 | @param stderr: boolean, read from stderr
151 | @param maxread: stop when max read bytes have been read (before timeout t kicks in) (-1: read all)
152 |
153 | Changes made wrt original:
154 | - add maxread here
155 | - set e to False by default
156 | """
157 | if maxread is None:
158 | maxread = -1
159 |
160 | if tr < 1:
161 | tr = 1
162 | x = time.time() + t
163 | y = []
164 | len_y = 0
165 | r = ""
166 | pr = p.recv
167 | if stderr:
168 | pr = p.recv_err
169 | while (maxread < 0 or len_y <= maxread) and (time.time() < x or r):
170 | r = pr(maxread)
171 | if r is None:
172 | if e:
173 | raise Exception(MESSAGE)
174 | else:
175 | break
176 | elif r:
177 | y.append(r)
178 | len_y += len(r)
179 | else:
180 | time.sleep(max((x - time.time()) / tr, 0))
181 | return b"".join(y)
182 |
183 |
184 | def send_all(p, data):
185 | """
186 | Send data to process p
187 | """
188 | allsent = 0
189 |
190 | data = data.encode()
191 |
192 | while len(data):
193 | sent = p.send(data)
194 | if sent is None:
195 | raise Exception(MESSAGE)
196 | allsent += sent
197 | data = memoryview(data)[sent:]
198 | return allsent
199 |
--------------------------------------------------------------------------------
/test/optcomplete.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2013-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Tests for the vsc.utils.optcomplete module and its use in generaloption.
28 |
29 | @author: Stijn De Weirdt (Ghent University)
30 | """
31 | import os
32 | import tempfile
33 | from vsc.install.testing import TestCase
34 | from vsc.utils import optcomplete
35 | from vsc.utils.optcomplete import Completer, CompleterMissingCallArgument
36 | from vsc.utils.optcomplete import NoneCompleter, ListCompleter, AllCompleter, KnownHostsCompleter
37 | from vsc.utils.optcomplete import FileCompleter, DirCompleter, RegexCompleter
38 | from vsc.utils.optcomplete import extract_word, gen_cmdline, OPTCOMPLETE_ENVIRONMENT
39 |
40 |
41 | class OptcompleteTest(TestCase):
42 | """Tests for optcomplete."""
43 |
44 | def setUp(self):
45 | """Create fake directory structure with files and directories"""
46 | self.basetemp = tempfile.mkdtemp()
47 | self.tempfiles = []
48 | self.tempdirs = []
49 |
50 | for _ in range(3):
51 | fhid, fn = tempfile.mkstemp(dir=self.basetemp)
52 | fh = os.fdopen(fhid, 'w')
53 | fh.write('')
54 | fh.close()
55 | self.tempfiles.append(fn)
56 |
57 | for _ in range(3):
58 | self.tempdirs.append(tempfile.mkdtemp(dir=self.basetemp))
59 |
60 | self.alltemp = self.tempfiles + self.tempdirs
61 | super().setUp()
62 |
63 | def tearDown(self):
64 | """cleanup testing"""
65 | for fn in self.tempfiles:
66 | os.remove(fn)
67 |
68 | for fn in self.tempdirs:
69 | os.rmdir(fn)
70 | os.rmdir(self.basetemp)
71 |
72 | def test_base_completer(self):
73 | """Test the base Completer class"""
74 | class NewCompleter(Completer):
75 | CALL_ARGS = ['x']
76 | CALL_ARGS_OPTIONAL = ['y']
77 |
78 | def _call(self, **kwargs):
79 | return sorted(kwargs.keys())
80 |
81 | saved_err = None
82 | nc = NewCompleter()
83 | # missing mandatory CALL_ARGS
84 | try:
85 | nc()
86 | except Exception as err:
87 | saved_err = err
88 |
89 | self.assertEqual(saved_err.__class__, CompleterMissingCallArgument)
90 |
91 | # proper usage : strip any non-mandatory or optional argument from kwargs
92 | res = nc(x=1, y=2, z=3)
93 | self.assertEqual(res, sorted(['x', 'y']))
94 |
95 | def test_none_completer(self):
96 | """Test NoneCompleter class"""
97 |
98 | # ignore all arguments
99 | nc = NoneCompleter()
100 | self.assertEqual(nc(x=1, y=1), [])
101 |
102 | def test_list_completer(self):
103 | """Test ListCompleter class"""
104 | # return list of strings
105 | initlist = ['original', 'list', 1]
106 | lc = ListCompleter(initlist)
107 | self.assertEqual(lc(), list(map(str, initlist)))
108 |
109 | def test_all_completer(self):
110 | """Test the AllCompleter class"""
111 | ac = AllCompleter()
112 | res = ac(pwd=self.basetemp)
113 | self.assertEqual([os.path.join(self.basetemp, name) for name in sorted(res)], sorted(self.alltemp))
114 |
115 | def test_known_hosts_completer(self):
116 | """Test KnownHostsCompleter"""
117 |
118 | kc = KnownHostsCompleter()
119 |
120 | # only proper bash support
121 | optcomplete.SHELL = optcomplete.BASH
122 | self.assertEqual(kc(), '_known_hosts')
123 |
124 | # any other must get back []
125 | optcomplete.SHELL = 'not a real shell'
126 | self.assertEqual(kc(), [])
127 |
128 | def test_file_completer(self):
129 | """Test FileCompleter"""
130 | # test bash
131 | optcomplete.SHELL = optcomplete.BASH
132 | fc = FileCompleter()
133 | self.assertEqual(fc(), '_filedir')
134 |
135 | fc = FileCompleter(['.a'])
136 | self.assertEqual(fc(), "_filedir '@(.a)'")
137 |
138 | def test_dir_completer(self):
139 | """Test DirCompleter"""
140 | # test bash
141 | optcomplete.SHELL = optcomplete.BASH
142 | dc = DirCompleter()
143 | self.assertEqual(dc(), '_filedir -d')
144 |
145 | def test_regex_completer(self):
146 | """Test RegexCompleter"""
147 | rc = RegexCompleter(['^tmp'])
148 | expected_res = self.tempfiles + self.tempdirs + [f"{p}{os.path.sep}" for p in self.tempdirs]
149 | self.assertEqual(sorted(rc(prefix=os.path.join(self.basetemp, 'tmp'))), sorted(expected_res))
150 |
151 | def test_extract_word(self):
152 | """Test the extract_word function"""
153 | testlines = {
154 | "extraire un mot d'une phrase": {
155 | 11: ('un', ''),
156 | 12: ('', 'mot'),
157 | 13: ('m', 'ot'),
158 | 14: ('mo', 't'),
159 | 0: ('', 'extraire'),
160 | 28: ('phrase', ''),
161 | 29: ('', ''),
162 | - 2: ('', ''),
163 | },
164 | "optcomplete-test do": {
165 | 19: ('do', ''),
166 | }
167 | }
168 |
169 | for line, totest in testlines.items():
170 | for pointer, res in totest.items():
171 | self.assertEqual(extract_word(line, pointer), res)
172 |
173 | def test_gen_cmdline(self):
174 | """Test generation of commndline"""
175 | partial = 'z'
176 | cmd_list = ['x', 'y', partial]
177 | cmdline = gen_cmdline(cmd_list, partial)
178 | for word in [OPTCOMPLETE_ENVIRONMENT, 'COMP_LINE', 'COMP_WORDS', 'COMP_CWORD', 'COMP_POINT']:
179 | self.assertTrue(f"{word}=" in cmdline)
180 |
181 |
182 | def suite():
183 | """ return all the tests"""
184 | return TestLoader().loadTestsFromTestCase(TestOptcomplete)
185 |
186 |
187 | if __name__ == '__main__':
188 | main()
189 |
--------------------------------------------------------------------------------
/test/rest.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2014-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Unit tests for the rest client.
28 |
29 | @author: Jens Timmerman (Ghent University)
30 | """
31 | import os
32 |
33 | from vsc.install.testing import TestCase
34 | from vsc.utils.rest import Client, RestClient
35 | from urllib.request import HTTPError
36 |
37 | # the user who's repo to test
38 | GITHUB_USER = "hpcugent"
39 | # the repo of this user to use in this test
40 | GITHUB_REPO = "testrepository"
41 | # Github username (optional)
42 | GITHUB_LOGIN = os.environ.get('VSC_GITHUB_LOGIN', None)
43 | # github auth token to use (optional)
44 | GITHUB_TOKEN = os.environ.get('VSC_GITHUB_TOKEN', None)
45 | # branch to test
46 | GITHUB_BRANCH = 'master'
47 |
48 |
49 | class RestClientTest(TestCase):
50 | """ small test for The RestClient
51 | This should not be to much, since there is an hourly limit of requests for the github api
52 | """
53 |
54 | def setUp(self):
55 | """setup"""
56 | super().setUp()
57 | self.client = RestClient('https://api.github.com', username=GITHUB_LOGIN, token=GITHUB_TOKEN)
58 |
59 | def test_client(self):
60 | """Do a test api call"""
61 | if GITHUB_TOKEN is None:
62 | print("Skipping test_client, since no GitHub token is available")
63 | return
64 |
65 | status, body = self.client.repos[GITHUB_USER][GITHUB_REPO].contents.a_directory['a_file.txt'].get()
66 | self.assertEqual(status, 200)
67 | # dGhpcyBpcyBhIGxpbmUgb2YgdGV4dAo= == 'this is a line of text' in base64 encoding
68 | self.assertEqual(body['content'].strip(), "dGhpcyBpcyBhIGxpbmUgb2YgdGV4dAo=")
69 |
70 | status, body = self.client.repos['hpcugent']['easybuild-framework'].pulls[1].get()
71 | self.assertEqual(status, 200)
72 | self.assertEqual(body['merge_commit_sha'], 'fba3e13815f3d2a9dfbd2f89f1cf678dd58bb1f1')
73 |
74 | def test_request_methods(self):
75 | """Test all request methods"""
76 | if GITHUB_TOKEN is None:
77 | print("Skipping test_request_methods, since no GitHub token is available")
78 | return
79 |
80 | status, headers = self.client.head()
81 | self.assertEqual(status, 200)
82 | self.assertTrue(headers)
83 | self.assertTrue('X-GitHub-Media-Type' in headers)
84 | try:
85 | status, body = self.client.user.emails.post(body='jens.timmerman@ugent.be')
86 | self.assertTrue(False, 'posting to unauthorized endpoint did not trhow a http error')
87 | except HTTPError:
88 | pass
89 | try:
90 | status, body = self.client.user.emails.delete(body='jens.timmerman@ugent.be')
91 | self.assertTrue(False, 'deleting to unauthorized endpoint did not trhow a http error')
92 | except HTTPError:
93 | pass
94 |
95 | def test_get_method(self):
96 | """A quick test of a GET to the github API"""
97 |
98 | status, body = self.client.users['hpcugent'].get()
99 | self.assertEqual(status, 200)
100 | self.assertEqual(body['login'], 'hpcugent')
101 | self.assertEqual(body['id'], 1515263)
102 |
103 | def test_get_connection(self):
104 | """Test for Client.get_connection."""
105 |
106 | client = Client('https://api.github.com')
107 |
108 | url = '/repos/hpcugent/vsc-base/pulls/296/comments'
109 | try:
110 | client.get_connection(Client.POST, url, body="{'body': 'test'}", headers={})
111 | self.assertTrue(False, "Trying to post a comment unauthorized should result in HTTPError")
112 | except HTTPError:
113 | pass
114 |
115 | def test_censor_request(self):
116 | """Test censor of requests"""
117 |
118 | client = Client('https://api.github.com')
119 |
120 | payload = {'username': 'vsc10001', 'password': 'potato', 'token': '123456'}
121 | payload_censored = client.censor_request(['password'], payload)
122 | self.assertEqual(
123 | payload_censored,
124 | {'username': 'vsc10001', 'password': '', 'token': '123456'},
125 | )
126 | payload_censored = client.censor_request(['password', 'token'], payload)
127 | self.assertEqual(
128 | payload_censored,
129 | {'username': 'vsc10001', 'password': '', 'token': ''},
130 | )
131 | payload_censored = client.censor_request(['something_else'], payload)
132 | self.assertEqual(payload_censored, payload)
133 |
134 | nondict_payload = [payload, 'more_payload']
135 | payload_censored = client.censor_request(['password'], nondict_payload)
136 | self.assertEqual(payload_censored, nondict_payload)
137 |
138 |
139 | class RestClientNoDecodeTest(TestCase):
140 | """ small test for The RestClient
141 | This should not be to much, since there is an hourly limit of requests for the github api
142 | """
143 |
144 | def setUp(self):
145 | """setup"""
146 | super().setUp()
147 | self.client = RestClient('https://api.github.com', username=GITHUB_LOGIN, token=GITHUB_TOKEN, decode=False)
148 |
149 | def test_client(self):
150 | """Do a test api call"""
151 | if GITHUB_TOKEN is None:
152 | print("Skipping test_client, since no GitHub token is available")
153 | return
154 |
155 | status, body = self.client.repos[GITHUB_USER][GITHUB_REPO].contents.a_directory['a_file.txt'].get()
156 | self.assertEqual(status, 200)
157 | # dGhpcyBpcyBhIGxpbmUgb2YgdGV4dAo= == 'this is a line of text' in base64 encoding
158 | self.assertEqual(body['content'].strip(), "dGhpcyBpcyBhIGxpbmUgb2YgdGV4dAo=")
159 |
160 | status, body = self.client.repos['hpcugent']['easybuild-framework'].pulls[1].get()
161 | self.assertEqual(status, 200)
162 | self.assertEqual(body['merge_commit_sha'], 'fba3e13815f3d2a9dfbd2f89f1cf678dd58bb1f1')
163 |
164 | def test_get_method(self):
165 | """A quick test of a GET to the github API"""
166 |
167 | status, body = self.client.users['hpcugent'].get()
168 | self.assertEqual(status, 200)
169 | self.assertTrue('"login":"hpcugent"' in body)
170 | self.assertTrue('"id":1515263' in body)
171 |
172 |
173 |
174 |
--------------------------------------------------------------------------------
/test/exceptions.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2015-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Unit tests for exceptions module.
28 |
29 | @author: Kenneth Hoste (Ghent University)
30 | """
31 | import os
32 | import re
33 | import tempfile
34 | from unittest import TestLoader, main
35 |
36 | from vsc.utils.exceptions import LoggedException, get_callers_logger
37 | from vsc.utils.fancylogger import getLogger, logToFile, logToScreen, getRootLoggerName, setLogFormat
38 | from vsc.install.testing import TestCase
39 |
40 |
41 | def raise_loggedexception(msg, *args, **kwargs):
42 | """Utility function: just raise a LoggedException."""
43 | raise LoggedException(msg, *args, **kwargs)
44 |
45 |
46 | class ExceptionsTest(TestCase):
47 | """Tests for exceptions module."""
48 |
49 | def test_loggedexception_defaultlogger(self):
50 | """Test LoggedException custom exception class."""
51 | fd, tmplog = tempfile.mkstemp()
52 | os.close(fd)
53 |
54 | # set log format, for each regex searching
55 | setLogFormat("%(name)s :: %(message)s")
56 |
57 | # if no logger is available, and no logger is specified, use default 'root' fancylogger
58 | logToFile(tmplog, enable=True)
59 | self.assertErrorRegex(LoggedException, 'BOOM', raise_loggedexception, 'BOOM')
60 | logToFile(tmplog, enable=False)
61 |
62 | log_re = re.compile(f"^{getRootLoggerName()} :: BOOM( \\(at .*:[0-9]+ in raise_loggedexception\\))?$", re.M)
63 | with open(tmplog) as f:
64 | logtxt = f.read()
65 | self.assertTrue(log_re.match(logtxt), f"{log_re.pattern} matches {logtxt}")
66 |
67 | # test formatting of message
68 | self.assertErrorRegex(LoggedException, 'BOOMBAF', raise_loggedexception, 'BOOM%s', 'BAF')
69 |
70 | # test log message that contains '%s' without any formatting arguments being passed
71 | # test formatting of message
72 | self.assertErrorRegex(LoggedException, "BOOM '%s'", raise_loggedexception, "BOOM '%s'")
73 |
74 | os.remove(tmplog)
75 |
76 | def test_loggedexception_specifiedlogger(self):
77 | """Test LoggedException custom exception class."""
78 | fd, tmplog = tempfile.mkstemp()
79 | os.close(fd)
80 |
81 | # set log format, for each regex searching
82 | setLogFormat("%(name)s :: %(message)s")
83 |
84 | logger1 = getLogger('testlogger_one')
85 |
86 | # if logger is specified, it should be used
87 | logToFile(tmplog, enable=True)
88 | self.assertErrorRegex(LoggedException, 'BOOM', raise_loggedexception, 'BOOM', logger=logger1)
89 | logToFile(tmplog, enable=False)
90 |
91 | rootlog = getRootLoggerName()
92 | log_re = re.compile(rf"^{rootlog}.testlogger_one :: BOOM( \(at .*:[0-9]+ in raise_loggedexception\))?$", re.M)
93 | with open(tmplog) as f:
94 | logtxt = f.read()
95 | self.assertTrue(log_re.match(logtxt), f"{log_re.pattern} matches {logtxt}")
96 |
97 | os.remove(tmplog)
98 |
99 | def test_loggedexception_callerlogger(self):
100 | """Test LoggedException custom exception class."""
101 | fd, tmplog = tempfile.mkstemp()
102 | os.close(fd)
103 |
104 | # set log format, for each regex searching
105 | setLogFormat("%(name)s :: %(message)s")
106 |
107 |
108 | # if no logger is specified, logger available in calling context should be used
109 | logToFile(tmplog, enable=True)
110 | self.assertErrorRegex(LoggedException, 'BOOM', raise_loggedexception, 'BOOM')
111 | logToFile(tmplog, enable=False)
112 |
113 | rootlog = getRootLoggerName()
114 | log_re = re.compile(rf"^{rootlog}(.testlogger_local)? :: BOOM( \(at .*:[0-9]+ in raise_loggedexception\))?$")
115 | with open(tmplog) as f:
116 | logtxt = f.read()
117 | self.assertTrue(log_re.match(logtxt), f"{log_re.pattern} matches {logtxt}")
118 |
119 | os.remove(tmplog)
120 |
121 | def test_loggedexception_location(self):
122 | """Test inclusion of location information in log message for LoggedException."""
123 | class TestException(LoggedException):
124 | LOC_INFO_TOP_PKG_NAMES = None
125 | INCLUDE_LOCATION = True
126 |
127 | def raise_testexception(msg, *args, **kwargs):
128 | """Utility function: just raise a TestException."""
129 | raise TestException(msg, *args, **kwargs)
130 |
131 | fd, tmplog = tempfile.mkstemp()
132 | os.close(fd)
133 |
134 | # set log format, for each regex searching
135 | setLogFormat("%(name)s :: %(message)s")
136 |
137 | # no location with default LOC_INFO_TOP_PKG_NAMES ([])
138 | logToFile(tmplog, enable=True)
139 | self.assertErrorRegex(LoggedException, 'BOOM', raise_testexception, 'BOOM')
140 | logToFile(tmplog, enable=False)
141 |
142 | rootlogname = getRootLoggerName()
143 |
144 | log_re = re.compile(f"^{rootlogname} :: BOOM$", re.M)
145 | with open(tmplog) as f:
146 | logtxt = f.read()
147 | self.assertTrue(log_re.match(logtxt), f"{log_re.pattern} matches {logtxt}")
148 |
149 | with open(tmplog, 'w') as f:
150 | f.write('')
151 |
152 | # location is included if LOC_INFO_TOP_PKG_NAMES is defined
153 | TestException.LOC_INFO_TOP_PKG_NAMES = ['vsc']
154 | logToFile(tmplog, enable=True)
155 | self.assertErrorRegex(LoggedException, 'BOOM', raise_testexception, 'BOOM')
156 | logToFile(tmplog, enable=False)
157 |
158 | log_re = re.compile(r"^%s :: BOOM \(at (?:.*?/)?vsc/install/testing.py:[0-9]+ in assertErrorRegex\)$" % rootlogname)
159 | with open(tmplog) as f:
160 | logtxt = f.read()
161 | self.assertTrue(log_re.match(logtxt), f"{log_re.pattern} matches {logtxt}")
162 |
163 | with open(tmplog, 'w') as f:
164 | f.write('')
165 |
166 | # absolute path of location is included if there's no match in LOC_INFO_TOP_PKG_NAMES
167 | TestException.LOC_INFO_TOP_PKG_NAMES = ['foobar']
168 | logToFile(tmplog, enable=True)
169 | self.assertErrorRegex(LoggedException, 'BOOM', raise_testexception, 'BOOM')
170 | logToFile(tmplog, enable=False)
171 |
172 | log_re = re.compile(r"^%s :: BOOM \(at (?:.*?/)?vsc/install/testing.py:[0-9]+ in assertErrorRegex\)$" % rootlogname)
173 | with open(tmplog) as f:
174 | logtxt = f.read()
175 | self.assertTrue(log_re.match(logtxt), f"{log_re.pattern} matches {logtxt}")
176 |
177 | os.remove(tmplog)
178 |
179 | def test_get_callers_logger(self):
180 | """Test get_callers_logger function."""
181 | # returns None if no logger is available
182 | self.assertEqual(get_callers_logger(), None)
183 |
184 | # find defined logger in caller's context
185 | logger = getLogger('foo')
186 | callers_logger = get_callers_logger()
187 | # result depends on whether tests were run under 'python' or 'python -O'
188 | self.assertTrue(callers_logger in [logger, None])
189 |
190 | # also works when logger is 'higher up'
191 | class Test:
192 | """Dummy test class"""
193 | def foo(self, logger=None):
194 | """Dummy test method, returns logger from calling context."""
195 | return get_callers_logger()
196 |
197 | test = Test()
198 | self.assertTrue(logger, [test.foo(), None])
199 |
200 | # closest logger to caller is preferred
201 | logger2 = getLogger(test.__class__.__name__)
202 | self.assertTrue(logger2 in [test.foo(logger=logger2), None])
203 |
204 | def suite():
205 | """ returns all the testcases in this module """
206 | return TestLoader().loadTestsFromTestCase(ExceptionsTest)
207 |
208 | if __name__ == '__main__':
209 | """Use this __main__ block to help write and test unittests"""
210 | main()
211 |
--------------------------------------------------------------------------------
/lib/vsc/utils/affinity.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2012-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Linux cpu affinity.
28 | - Based on C{sched.h} and C{bits/sched.h},
29 | - see man pages for C{sched_getaffinity} and C{sched_setaffinity}
30 | - also provides a C{cpuset} class to convert between human readable cpusets and the bit version
31 | Linux priority
32 | - Based on sys/resources.h and bits/resources.h see man pages for
33 | C{getpriority} and C{setpriority}
34 |
35 | @author: Stijn De Weirdt (Ghent University)
36 | """
37 |
38 | import ctypes
39 | import logging
40 | import os
41 | from ctypes.util import find_library
42 |
43 | _libc_lib = find_library("c")
44 | _libc = ctypes.cdll.LoadLibrary(_libc_lib)
45 |
46 | # /* Type for array elements in 'cpu_set_t'. */
47 | # typedef unsigned long int __cpu_mask;
48 | cpu_mask_t = ctypes.c_ulong
49 |
50 | ##define __CPU_SETSIZE 1024
51 | ##define __NCPUBITS (8 * sizeof(__cpu_mask))
52 | CPU_SETSIZE = 1024
53 | NCPUBITS = 8 * ctypes.sizeof(cpu_mask_t)
54 | NMASKBITS = CPU_SETSIZE // NCPUBITS
55 |
56 | # /* Priority limits. */
57 | ##define PRIO_MIN -20 /* Minimum priority a process can have. */
58 | ##define PRIO_MAX 20 /* Maximum priority a process can have. */
59 | PRIO_MIN = -20
60 | PRIO_MAX = 20
61 |
62 | # /* The type of the WHICH argument to `getpriority' and `setpriority',
63 | # indicating what flavor of entity the WHO argument specifies. * /
64 | # enum __priority_which
65 | ##{
66 | # PRIO_PROCESS = 0, /* WHO is a process ID. * /
67 | ##define PRIO_PROCESS PRIO_PROCESS
68 | # PRIO_PGRP = 1, /* WHO is a process group ID. * /
69 | ##define PRIO_PGRP PRIO_PGRP
70 | # PRIO_USER = 2 /* WHO is a user ID. * /
71 | ##define PRIO_USER PRIO_USER
72 | ##};
73 | PRIO_PROCESS = 0
74 | PRIO_PGRP = 1
75 | PRIO_USER = 2
76 |
77 | # /* using pid_t for __pid_t */
78 | # typedef unsigned pid_t;
79 | pid_t = ctypes.c_uint
80 |
81 | ##if defined __USE_GNU && !defined __cplusplus
82 | # typedef enum __rlimit_resource __rlimit_resource_t;
83 | # typedef enum __rusage_who __rusage_who_t;
84 | # typedef enum __priority_which __priority_which_t;
85 | ##else
86 | # typedef int __rlimit_resource_t;
87 | # typedef int __rusage_who_t;
88 | # typedef int __priority_which_t;
89 | ##endif
90 | priority_which_t = ctypes.c_int
91 |
92 | ## typedef __u_int __id_t;
93 | id_t = ctypes.c_uint
94 |
95 |
96 | # /* Data structure to describe CPU mask. */
97 | # typedef struct
98 | # {
99 | # __cpu_mask __bits[__NMASKBITS];
100 | # } cpu_set_t;
101 | class cpu_set_t(ctypes.Structure):
102 | """
103 | Class that implements the cpu_set_t struct
104 | also provides some methods to convert between bit representation and soem human readable format
105 |
106 | Example usage:
107 | cs = cpu_set_t()
108 | print("__bits " + cs.__bits)
109 | print("sizeof cpu_set_t " + ctypes.sizeof(cs))
110 | """
111 |
112 | _fields_ = [("__bits", cpu_mask_t * NMASKBITS)]
113 |
114 | def __init__(self, *args, **kwargs):
115 | super().__init__(*args, **kwargs)
116 | self.cpus = None
117 |
118 | def __str__(self):
119 | return self.convert_bits_hr()
120 |
121 | def convert_hr_bits(self, txt):
122 | """Convert human readable text into bits"""
123 | self.cpus = [0] * CPU_SETSIZE
124 | for rng in txt.split(","):
125 | # always at least 2 elements: twice the same or start,end,start,end
126 | indices = [int(x) for x in rng.split("-")] * 2
127 |
128 | # sanity check
129 | if indices[1] < indices[0]:
130 | logging.error("convert_hr_bits: end is lower then start in '%s'", rng)
131 | raise ValueError("convert_hr_bits: end is lower then start")
132 | elif indices[0] < 0:
133 | logging.error("convert_hr_bits: negative start in '%s'", rng)
134 | raise ValueError("convert_hr_bits: negative start")
135 | elif indices[1] > CPU_SETSIZE + 1: # also covers start, since end > start
136 | logging.error("convert_hr_bits: end larger then max %s in '%s'", CPU_SETSIZE, rng)
137 | raise ValueError("convert_hr_bits: end larger then max")
138 |
139 | self.cpus[indices[0] : indices[1] + 1] = [1] * (indices[1] + 1 - indices[0])
140 | logging.debug("convert_hr_bits: converted %s into cpus %s", txt, self.cpus)
141 |
142 | def convert_bits_hr(self):
143 | """Convert __bits into human readable text"""
144 | if self.cpus is None:
145 | self.get_cpus()
146 | cpus_index = [idx for idx, cpu in enumerate(self.cpus) if cpu == 1]
147 | prev = -2 # not adjacent to 0 !
148 | parsed_idx = []
149 | for idx in cpus_index:
150 | if prev + 1 < idx:
151 | parsed_idx.append(f"{idx}")
152 | else:
153 | first_idx = parsed_idx[-1].split("-")[0]
154 | parsed_idx[-1] = f"{first_idx}-{idx}"
155 | prev = idx
156 | return ",".join(parsed_idx)
157 |
158 | def get_cpus(self):
159 | """Convert bits in list len == CPU_SETSIZE
160 | Use 1 / 0 per cpu
161 | """
162 | self.cpus = []
163 | for bitmask in getattr(self, "__bits"):
164 | for _ in range(NCPUBITS):
165 | self.cpus.append(bitmask & 1)
166 | bitmask >>= 1
167 | return self.cpus
168 |
169 | def set_cpus(self, cpus_list):
170 | """Given list, set it as cpus"""
171 | nr_cpus = len(cpus_list)
172 | if nr_cpus > CPU_SETSIZE:
173 | logging.warning(
174 | "set_cpus: length cpu list %s is larger then cpusetsize %s. Truncating to cpusetsize",
175 | nr_cpus,
176 | CPU_SETSIZE,
177 | )
178 | cpus_list = cpus_list[:CPU_SETSIZE]
179 | elif nr_cpus < CPU_SETSIZE:
180 | cpus_list.extend([0] * (CPU_SETSIZE - nr_cpus))
181 |
182 | self.cpus = cpus_list
183 |
184 | def set_bits(self, cpus=None):
185 | """Given self.cpus, set the bits"""
186 | if cpus is not None:
187 | self.set_cpus(cpus)
188 | __bits = getattr(self, "__bits")
189 | prev_cpus = list(map(int, self.cpus))
190 | for idx in range(NMASKBITS):
191 | cpus = [
192 | 2**cpuidx for cpuidx, val in enumerate(self.cpus[idx * NCPUBITS : (idx + 1) * NCPUBITS]) if val == 1
193 | ]
194 | __bits[idx] = cpu_mask_t(sum(cpus))
195 | # sanity check
196 | if prev_cpus == self.get_cpus():
197 | logging.debug("set_bits: new set to %s", self.convert_bits_hr())
198 | else:
199 | # get_cpus() rescans
200 | logging.error(
201 | "set_bits: something went wrong: previous cpus %s; current ones %s", prev_cpus[:20], self.cpus[:20]
202 | )
203 | raise ValueError("set_bits: something went wrong: previous cpus / current ones")
204 |
205 | def str_cpus(self):
206 | """Return a string representation of the cpus"""
207 | if self.cpus is None:
208 | self.get_cpus()
209 | return "".join([f"{int(x)}" for x in self.cpus])
210 |
211 |
212 | # /* Get the CPU affinity for a task */
213 | # extern int sched_getaffinity (pid_t __pid, size_t __cpusetsize,
214 | # cpu_set_t *__cpuset);
215 | def sched_getaffinity(cs=None, pid=None):
216 | """
217 | Get the affinity
218 |
219 | Example usage:
220 |
221 | x = sched_getaffinity()
222 | print("x " + x)
223 | hr_mask = "1-5,7,9,10-15"
224 | print(hr_mask + ' ' + x.convert_hr_bits(hr_mask))
225 | print(x)
226 | x.set_bits()
227 | print(x)
228 |
229 | sched_setaffinity(x)
230 | print(sched_getaffinity())
231 |
232 | x.convert_hr_bits("1")
233 | x.set_bits()
234 | sched_setaffinity(x)
235 | y = sched_getaffinity()
236 | print(x + ' ' + y)
237 | """
238 | if cs is None:
239 | cs = cpu_set_t()
240 | if pid is None:
241 | pid = os.getpid()
242 |
243 | ec = _libc.sched_getaffinity(pid_t(pid), ctypes.sizeof(cpu_set_t), ctypes.pointer(cs))
244 | if ec == 0:
245 | logging.debug("sched_getaffinity for pid %s returned cpuset %s", pid, cs)
246 | else:
247 | logging.error("sched_getaffinity failed for pid %s ec %s", pid, ec)
248 | return cs
249 |
250 |
251 | # /* Set the CPU affinity for a task */
252 | # extern int sched_setaffinity (pid_t __pid, size_t __cpusetsize,
253 | # cpu_set_t *__cpuset);
254 | def sched_setaffinity(cs, pid=None):
255 | """Set the affinity"""
256 | if pid is None:
257 | pid = os.getpid()
258 |
259 | ec = _libc.sched_setaffinity(pid_t(pid), ctypes.sizeof(cpu_set_t), ctypes.pointer(cs))
260 | if ec == 0:
261 | logging.debug("sched_setaffinity for pid %s and cpuset %s", pid, cs)
262 | else:
263 | logging.error("sched_setaffinity failed for pid %s cpuset %s ec %s", pid, cs, ec)
264 |
265 |
266 | # /* Get index of currently used CPU. */
267 | # extern int sched_getcpu (void) __THROW;
268 | def sched_getcpu():
269 | """
270 | Get currently used cpu
271 |
272 | Example usage:
273 | print(sched_getcpu())
274 | """
275 | return _libc.sched_getcpu()
276 |
277 |
278 | # /* Return the highest priority of any process specified by WHICH and WHO
279 | # (see above); if WHO is zero, the current process, process group, or user
280 | # (as specified by WHO) is used. A lower priority number means higher
281 | # priority. Priorities range from PRIO_MIN to PRIO_MAX (above). */
282 | # extern int getpriority (__priority_which_t __which, id_t __who) __THROW;
283 | #
284 | # /* Set the priority of all processes specified by WHICH and WHO (see above)
285 | # to PRIO. Returns 0 on success, -1 on errors. */
286 | # extern int setpriority (__priority_which_t __which, id_t __who, int __prio)
287 | # __THROW;
288 | def getpriority(which=None, who=None):
289 | """
290 | Get the priority
291 |
292 | Example usage:
293 | # resources
294 | # nice -n 5 python affinity.py prints 5 here
295 | currentprio = getpriority()
296 | print("getpriority " + currentprio)
297 | """
298 | if which is None:
299 | which = PRIO_PROCESS
300 | elif which not in (
301 | PRIO_PROCESS,
302 | PRIO_PGRP,
303 | PRIO_USER,
304 | ):
305 | logging.error("getpriority: which %s not in correct range", which)
306 | raise ValueError("getpriority: which not in correct range")
307 | if who is None:
308 | who = 0 # current which-ever
309 | prio = _libc.getpriority(
310 | priority_which_t(which),
311 | id_t(who),
312 | )
313 | logging.debug("getpriority prio %s for which %s who %s", prio, which, who)
314 |
315 | return prio
316 |
317 |
318 | def setpriority(prio, which=None, who=None):
319 | """
320 | Set the priority (aka nice)
321 |
322 | Example usage:
323 | newprio = 10
324 | setpriority(newprio)
325 | newcurrentprio = getpriority()
326 | print("getpriority " + newcurrentprio)
327 | assert newcurrentprio == newprio
328 | """
329 | if which is None:
330 | which = PRIO_PROCESS
331 | elif which not in (
332 | PRIO_PROCESS,
333 | PRIO_PGRP,
334 | PRIO_USER,
335 | ):
336 | logging.error("setpriority: which %s not in correct range", which)
337 | raise ValueError("setpriority: which not in correct range")
338 | if who is None:
339 | who = 0 # current which-ever
340 |
341 | try:
342 | prio = int(prio)
343 | except ValueError:
344 | logging.error("setpriority: failed to convert priority %s into int", prio)
345 | raise ValueError("setpriority: failed to convert priority into int")
346 |
347 | if prio < PRIO_MIN or prio > PRIO_MAX:
348 | logging.error("setpriority: prio not in allowed range MIN %s MAX %s", PRIO_MIN, PRIO_MAX)
349 | raise ValueError("setpriority: prio not in allowed range MIN MAX")
350 |
351 | ec = _libc.setpriority(priority_which_t(which), id_t(who), ctypes.c_int(prio))
352 | if ec == 0:
353 | logging.debug("setpriority for which %s who %s prio %s", which, who, prio)
354 | else:
355 | logging.error("setpriority failed for which %s who %s prio %s", which, who, prio)
356 |
--------------------------------------------------------------------------------
/lib/vsc/utils/dateandtime.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2012-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Module with various convenience functions and classes to deal with date, time and timezone
28 |
29 | @author: Stijn De Weirdt (Ghent University)
30 | """
31 |
32 | import calendar
33 | import re
34 | import time as _time
35 | from datetime import tzinfo, timedelta, datetime, date
36 |
37 |
38 | TODAY = "TODAY"
39 | TOMORROW = "TOMORROW"
40 | YESTERDAY = "YESTERDAY"
41 |
42 | BEGIN = "BEGIN"
43 | END = "END"
44 |
45 |
46 | class FancyMonth:
47 | """Convenience class for month math"""
48 |
49 | def __init__(self, tmpdate=None, year=None, month=None, day=None):
50 | """Initialise the month based on first day of month of tmpdate"""
51 |
52 | if tmpdate is None:
53 | tmpdate = date.today()
54 |
55 | if day is None:
56 | day = tmpdate.day
57 | if month is None:
58 | month = tmpdate.month
59 | if year is None:
60 | year = tmpdate.year
61 |
62 | self.date = date(year, month, day)
63 |
64 | self.first = None
65 | self.last = None
66 | self.nrdays = None
67 |
68 | # when calculating deltas, include non-full months
69 | # eg when True, nr of months between last day of month
70 | # and first day of following month is 2
71 | self.include = True
72 |
73 | self.set_details()
74 |
75 | def set_details(self):
76 | """Get first/last day of the month of date"""
77 |
78 | class MyCalendar:
79 | """Backport minimal calendar.Calendar code from 2.7 to support itermonthdays in 2.4"""
80 |
81 | def __init__(self, firstweekday=0):
82 | self.firstweekday = firstweekday # 0 = Monday, 6 = Sunday
83 |
84 | def itermonthdates(self, year, month):
85 | """
86 | Return an iterator for one month. The iterator will yield datetime.date
87 | values and will always iterate through complete weeks, so it will yield
88 | dates outside the specified month.
89 | """
90 | _date = date(year, month, 1)
91 | # Go back to the beginning of the week
92 | days = (_date.weekday() - self.firstweekday) % 7
93 | _date -= timedelta(days=days)
94 | oneday = timedelta(days=1)
95 | while True:
96 | yield _date
97 | _date += oneday
98 | if _date.month != month and _date.weekday() == self.firstweekday:
99 | break
100 |
101 | def itermonthdays(self, year, month):
102 | """
103 | Like itermonthdates(), but will yield day numbers. For days outside
104 | the specified month the day number is 0.
105 | """
106 | for _date in self.itermonthdates(year, month):
107 | if _date.month != month:
108 | yield 0
109 | else:
110 | yield _date.day
111 |
112 | if "Calendar" in dir(calendar): # py2.5+
113 | c = calendar.Calendar()
114 | else:
115 | c = MyCalendar()
116 | self.nrdays = len([x for x in c.itermonthdays(self.date.year, self.date.month) if x > 0])
117 |
118 | self.first = date(self.date.year, self.date.month, 1)
119 |
120 | self.last = date(self.date.year, self.date.month, self.nrdays)
121 |
122 | def get_start_end(self, otherdate):
123 | """Return tuple date and otherdate ordered oldest first"""
124 | if self.date > otherdate:
125 | start = otherdate
126 | end = self.date
127 | else:
128 | start = self.date
129 | end = otherdate
130 |
131 | return start, end
132 |
133 | def number(self, otherdate):
134 | """Calculate the number of months between this month (date actually) and otherdate"""
135 | if self.include is False:
136 | msg = "number: include=False not implemented"
137 | raise NotImplementedError(msg)
138 | else:
139 | startdate, enddate = self.get_start_end(otherdate)
140 |
141 | if startdate == enddate:
142 | nr = 0
143 | else:
144 | nr = (enddate.year - startdate.year) * 12 + enddate.month - startdate.month + 1
145 |
146 | return nr
147 |
148 | def get_other(self, shift=-1):
149 | """Return month that is shifted shift months: negative integer is in past, positive is in future"""
150 | new = self.date.year * 12 + self.date.month - 1 + shift
151 | return self.__class__(date(new // 12, new % 12 + 1, 1))
152 |
153 | def interval(self, otherdate):
154 | """Return time ordered list of months between date and otherdate"""
155 | if self.include is False:
156 | msg = "interval: include=False not implemented"
157 | raise NotImplementedError(msg)
158 | else:
159 | nr = self.number(otherdate)
160 | startdate, _ = self.get_start_end(otherdate)
161 |
162 | start = self.__class__(startdate)
163 | all_dates = [start.get_other(m) for m in range(nr)]
164 |
165 | return all_dates
166 |
167 | def parser(self, txt):
168 | """Based on strings, return date: eg BEGINTHIS returns first day of the current month"""
169 | supportedtime = (
170 | BEGIN,
171 | END,
172 | )
173 | supportedshift = ["THIS", "LAST", "NEXT"]
174 | regtxt = rf"^({'|'.join(supportedtime)})({'|'.join(supportedshift)})?"
175 |
176 | reseervedregexp = re.compile(regtxt)
177 | reg = reseervedregexp.search(txt)
178 | if not reg:
179 | msg = f"parse: no match for regexp {regtxt} for txt {txt}"
180 | raise ValueError(msg)
181 |
182 | shifttxt = reg.group(2)
183 | if shifttxt is None or shifttxt == "THIS":
184 | shift = 0
185 | elif shifttxt == "LAST":
186 | shift = -1
187 | elif shifttxt == "NEXT":
188 | shift = 1
189 | else:
190 | msg = f"parse: unknown shift {shifttxt} (supported: {supportedshift})"
191 | raise ValueError(msg)
192 |
193 | nm = self.get_other(shift)
194 |
195 | timetxt = reg.group(1)
196 | if timetxt == BEGIN:
197 | res = nm.first
198 | elif timetxt == END:
199 | res = nm.last
200 | else:
201 | msg = f"parse: unknown time {timetxt} (supported: {supportedtime})"
202 | raise ValueError(msg)
203 |
204 | return res
205 |
206 |
207 | def date_parser(txt):
208 | """Parse txt
209 |
210 | @type txt: string
211 |
212 | @param txt: date to be parsed. Usually in C{YYYY-MM-DD} format,
213 | but also C{(BEGIN|END)(THIS|LAST|NEXT)MONTH}, or even
214 | C{(BEGIN | END)(JANUARY | FEBRUARY | MARCH | APRIL | MAY | JUNE | JULY | AUGUST | SEPTEMBER | OCTOBER | NOVEMBER |
215 | DECEMBER)}
216 | """
217 |
218 | reserveddate = (
219 | TODAY,
220 | TOMORROW,
221 | YESTERDAY,
222 | )
223 | testsupportedmonths = [txt.endswith(calendar.month_name[x].upper()) for x in range(1, 13)]
224 |
225 | if txt.endswith("MONTH"):
226 | m = FancyMonth()
227 | res = m.parser(txt)
228 | elif any(testsupportedmonths):
229 | # set day=1 or this will fail on days with an index more then the count of days then the month you want to parse
230 | # e.g. will fail on 31'st when trying to parse april
231 | m = FancyMonth(month=testsupportedmonths.index(True) + 1, day=1)
232 | res = m.parser(txt)
233 | elif txt in reserveddate:
234 | if txt in (
235 | TODAY,
236 | TOMORROW,
237 | YESTERDAY,
238 | ):
239 | m = FancyMonth()
240 | res = m.date
241 |
242 | if txt in (
243 | TOMORROW,
244 | YESTERDAY,
245 | ):
246 | thisday = datetime(res.year, res.month, res.day)
247 | oneday = timedelta(days=1)
248 | if txt == TOMORROW:
249 | thisday += oneday
250 | elif txt == YESTERDAY:
251 | thisday -= oneday
252 | res = thisday.date()
253 | else:
254 | msg = f"dateparser: unimplemented reservedword {txt}"
255 | raise NotImplementedError(msg)
256 | else:
257 | try:
258 | datetuple = [int(x) for x in txt.split("-")]
259 | res = date(*datetuple)
260 | except ValueError:
261 | msg = (
262 | f"dateparser: failed on '{txt}' date txt expects a YYYY-MM-DD format or "
263 | f"reserved words {','.join(reserveddate)}"
264 | )
265 | raise ValueError(msg)
266 |
267 | return res
268 |
269 |
270 | def datetime_parser(txt):
271 | """Parse txt: tmpdate YYYY-MM-DD HH:MM:SS.mmmmmm in datetime.datetime
272 | - date part is parsed with date_parser
273 | """
274 | tmpts = txt.split(" ")
275 | tmpdate = date_parser(tmpts[0])
276 |
277 | datetuple = [tmpdate.year, tmpdate.month, tmpdate.day]
278 | res = datetime(*datetuple)
279 |
280 | if len(tmpts) > 1:
281 | hour = tmpts[1]
282 |
283 | if hour == BEGIN:
284 | # this is a noop
285 | pass
286 | elif hour == END:
287 | res += timedelta(days=1, seconds=-1)
288 | else:
289 | hms = hour.split(":")
290 |
291 | try:
292 | sects = hms[2].split(".")
293 | except (AttributeError, IndexError):
294 | sects = [0]
295 |
296 | # add hour, minutes, seconds
297 | res += timedelta(hours=int(hms[0]), minutes=int(hms[1]), seconds=int(sects[0]))
298 | if len(sects) > 1:
299 | res += timedelta(microseconds=int((sects[1] + "0" * 6)[:6]))
300 |
301 | return res
302 |
303 |
304 | def timestamp_parser(timestamp):
305 | """Parse timestamp to datetime"""
306 | return datetime.fromtimestamp(float(timestamp))
307 |
308 |
309 | #
310 | # example code from http://docs.python.org/library/datetime.html
311 | # Implements Local, the local timezone
312 | #
313 |
314 | ZERO = timedelta(0)
315 | HOUR = timedelta(hours=1)
316 |
317 |
318 | # A UTC class.
319 | class UTC(tzinfo):
320 | """UTC"""
321 |
322 | def utcoffset(self, dt): # pylint:disable=unused-argument
323 | return ZERO
324 |
325 | def tzname(self, dt): # pylint:disable=unused-argument
326 | return "UTC"
327 |
328 | def dst(self, dt): # pylint:disable=unused-argument
329 | return ZERO
330 |
331 |
332 | utc = UTC()
333 |
334 |
335 | class FixedOffset(tzinfo):
336 | """Fixed offset in minutes east from UTC.
337 |
338 | This is a class for building tzinfo objects for fixed-offset time zones.
339 | Note that FixedOffset(0, "UTC") is a different way to build a
340 | UTC tzinfo object.
341 | """
342 |
343 | def __init__(self, offset, name):
344 | self.__offset = timedelta(minutes=offset)
345 | self.__name = name
346 |
347 | def utcoffset(self, dt): # pylint:disable=unused-argument
348 | return self.__offset
349 |
350 | def tzname(self, dt): # pylint:disable=unused-argument
351 | return self.__name
352 |
353 | def dst(self, dt): # pylint:disable=unused-argument
354 | return ZERO
355 |
356 |
357 | STDOFFSET = timedelta(seconds=-_time.timezone)
358 | if _time.daylight:
359 | DSTOFFSET = timedelta(seconds=-_time.altzone)
360 | else:
361 | DSTOFFSET = STDOFFSET
362 |
363 | DSTDIFF = DSTOFFSET - STDOFFSET
364 |
365 |
366 | class LocalTimezone(tzinfo):
367 | """
368 | A class capturing the platform's idea of local time.
369 | """
370 |
371 | def utcoffset(self, dt):
372 | if self._isdst(dt):
373 | return DSTOFFSET
374 | else:
375 | return STDOFFSET
376 |
377 | def dst(self, dt):
378 | if self._isdst(dt):
379 | return DSTDIFF
380 | else:
381 | return ZERO
382 |
383 | def tzname(self, dt):
384 | return _time.tzname[self._isdst(dt)]
385 |
386 | def _isdst(self, dt):
387 | tt = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.weekday(), 0, 0)
388 | stamp = _time.mktime(tt)
389 | tt = _time.localtime(stamp)
390 | return tt.tm_isdst > 0
391 |
392 |
393 | Local = LocalTimezone()
394 |
--------------------------------------------------------------------------------
/lib/vsc/utils/mail.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2012-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Wrapper around the standard Python mail library.
28 |
29 | - Send a plain text message
30 | - Send an HTML message, with a plain text alternative
31 |
32 | @author: Andy Georges (Ghent University)
33 | """
34 |
35 | import logging
36 | import re
37 | import smtplib
38 | import ssl
39 | from configparser import ConfigParser
40 | from email.mime.multipart import MIMEMultipart
41 | from email.mime.text import MIMEText
42 | from email.mime.image import MIMEImage
43 |
44 |
45 | class VscMailError(Exception):
46 | """Raised if the sending of an email fails for some reason."""
47 |
48 | def __init__(self, mail_host=None, mail_to=None, mail_from=None, mail_subject=None, err=None):
49 | """Initialisation.
50 |
51 | @type mail_host: string
52 | @type mail_to: list of strings (or string, but deprecated)
53 | @type mail_from: string
54 | @type mail_subject: string
55 | @type err: Exception subclass
56 |
57 | @param mail_host: the SMTP host for actually sending the mail.
58 | @param mail_to: a well-formed email address of the recipient.
59 | @param mail_from: a well-formed email address of the sender.
60 | @param mail_subject: the subject of the mail.
61 | @param err: the original exception, if any.
62 | """
63 | self.mail_host = mail_host
64 | self.mail_to = mail_to
65 | self.mail_from = mail_from
66 | self.mail_subject = mail_subject
67 | self.err = err
68 |
69 |
70 | class VscMail:
71 | """Class providing functionality to send out mail."""
72 |
73 | def __init__(
74 | self,
75 | mail_host="",
76 | mail_port=587,
77 | smtp_auth_user=None,
78 | smtp_auth_password=None,
79 | smtp_use_starttls=False,
80 | mail_config=None,
81 | ):
82 | """
83 | - If there is a config file provided, its values take precedence over the arguments passed to __init__
84 | - If the mail_host is of the format host:port, that port takes precedence over mail_port
85 | """
86 |
87 | mail_options = ConfigParser()
88 | if mail_config:
89 | logging.info("Reading config file: %s", mail_config)
90 | with open(mail_config) as mc:
91 | mail_options.read_file(mc)
92 |
93 | # we can have cases where the host part is actually host:port
94 | _mail_host = mail_options.get("MAIN", "smtp", fallback=mail_host)
95 | try:
96 | self.mail_host, _mail_port = _mail_host.split(":")
97 | except ValueError:
98 | self.mail_host = _mail_host
99 | _mail_port = mail_options.get("MAIN", "mail_port", fallback=mail_port)
100 |
101 | self.mail_port = int(_mail_port)
102 | self.smtp_auth_user = mail_options.get("MAIN", "smtp_auth_user", fallback=smtp_auth_user)
103 | self.smtp_auth_password = mail_options.get("MAIN", "smtp_auth_password", fallback=smtp_auth_password)
104 | self.smtp_use_starttls = mail_options.get("MAIN", "smtp_use_starttls", fallback=smtp_use_starttls)
105 |
106 | def _connect(self):
107 | """
108 | Connect to the mail host on the given port.
109 |
110 | If provided, use authentication and TLS.
111 | """
112 | logging.debug("Using mail host %s, mail port %s", self.mail_host, self.mail_port)
113 | s = smtplib.SMTP(host=self.mail_host, port=self.mail_port)
114 |
115 | if self.smtp_use_starttls:
116 | context = ssl.create_default_context()
117 | s.starttls(context=context)
118 | logging.debug("Started TLS connection")
119 | elif not self.mail_host:
120 | s.connect()
121 |
122 | if self.smtp_auth_user and self.smtp_auth_password:
123 | s.login(user=self.smtp_auth_user, password=self.smtp_auth_password)
124 | logging.debug("Authenticated")
125 |
126 | return s
127 |
128 | def _send(self, mail_from, mail_to, mail_subject, msg):
129 | """Actually send the mail.
130 |
131 | @type mail_from: string representing the sender.
132 | @type mail_to: list of strings (or string, but deprecated) representing the recipient.
133 | @type mail_subject: string representing the subject.
134 | @type msg: MIME message.
135 | """
136 |
137 | try:
138 | s = self._connect()
139 |
140 | try:
141 | s.sendmail(mail_from, mail_to, msg.as_string())
142 | except smtplib.SMTPHeloError as err:
143 | logging.error("Cannot get a proper response from the SMTP host %s: %s", self.mail_host, err)
144 | raise
145 | except smtplib.SMTPRecipientsRefused as err:
146 | logging.error("All recipients were refused by SMTP host %s [%s]: %s", self.mail_host, mail_to, err)
147 | raise
148 | except smtplib.SMTPSenderRefused as err:
149 | logging.error("Sender was refused by SMTP host %s [%s]: %s", self.mail_host, mail_from, err)
150 | raise
151 | except smtplib.SMTPDataError as err:
152 | logging.error("Data error: %s", err)
153 | raise
154 |
155 | except smtplib.SMTPConnectError as err:
156 | logging.exception("Cannot connect to the SMTP host %s", self.mail_host)
157 | raise VscMailError(
158 | mail_host=self.mail_host, mail_to=mail_to, mail_from=mail_from, mail_subject=mail_subject, err=err
159 | )
160 | except Exception as err:
161 | logging.exception("Some unknown exception occurred in VscMail.sendTextMail. Raising a VscMailError.")
162 | raise VscMailError(
163 | mail_host=self.mail_host, mail_to=mail_to, mail_from=mail_from, mail_subject=mail_subject, err=err
164 | )
165 | else:
166 | s.quit()
167 |
168 | def sendTextMail(self, mail_to, mail_from, reply_to, mail_subject, message, cc=None, bcc=None):
169 | """Send out the given message by mail to the given recipient(s).
170 |
171 | @type mail_to: list
172 | @type mail_from: string
173 | @type reply_to: string
174 | @type mail_subject: string
175 | @type message: string
176 | @type cc: list
177 | @type bcc: list
178 |
179 | @param mail_to: a list of valid email addresses
180 | @param mail_from: a valid sender email address.
181 | @param reply_to: a valid email address for the (potential) replies.
182 | @param mail_subject: the subject of the email.
183 | @param message: the body of the mail.
184 | @param cc: a list of valid CC email addresses
185 | @param bcc: a list of valid BCC email addresses
186 | """
187 |
188 | # deprecated: single email as string
189 | if isinstance(mail_to, str):
190 | logging.warning("Deprecated, mail_to passed as string. Should be list of strings.")
191 | mail_to = [mail_to]
192 |
193 | logging.info("Sending mail [%s] to %s.", mail_subject, mail_to)
194 |
195 | msg = MIMEText(message, "plain", "utf-8")
196 | msg["Subject"] = mail_subject
197 | msg["From"] = mail_from
198 | msg["To"] = ",".join(mail_to)
199 |
200 | if cc:
201 | logging.info("Sending mail [%s] in CC to %s.", mail_subject, cc)
202 | msg["Cc"] = cc
203 | mail_to.extend(cc)
204 |
205 | if bcc:
206 | logging.info("Sending mail [%s] in BCC to %s.", mail_subject, bcc)
207 | # do *not* set msg['Bcc'] to avoid leaking the BCC email address to all recipients
208 | mail_to.extend(bcc)
209 |
210 | if reply_to is None:
211 | reply_to = mail_from
212 | msg["Reply-to"] = reply_to
213 |
214 | self._send(mail_from, mail_to, mail_subject, msg)
215 |
216 | def _replace_images_cid(self, html, images):
217 | """Replaces all occurences of the src="IMAGE" with src="cid:IMAGE" in the provided html argument.
218 |
219 | @type html: string
220 | @type images: list of strings
221 |
222 | @param html: HTML data, containing image tags for each of the provided images
223 | @param images: references to the images occuring in the HTML payload
224 |
225 | @return: the altered HTML string.
226 | """
227 |
228 | for im in images:
229 | re_src = re.compile(f'src="{im}"')
230 | (html, count) = re_src.subn(f'src="cid:{im}"', html)
231 | if count == 0:
232 | logging.error("Could not find image %s in provided HTML.", im)
233 | raise VscMailError("Could not find image")
234 |
235 | return html
236 |
237 | def sendHTMLMail(
238 | self,
239 | mail_to,
240 | mail_from,
241 | reply_to,
242 | mail_subject,
243 | html_message,
244 | text_alternative,
245 | images=None,
246 | css=None,
247 | cc=None,
248 | bcc=None,
249 | ):
250 | """
251 | Send an HTML email message, encoded in a MIME/multipart message.
252 |
253 | The images and css are included in the message, and should be provided separately.
254 |
255 | @type mail_to: list
256 | @type mail_from: string
257 | @type reply_to: string
258 | @type mail_subject: string
259 | @type html_message: string
260 | @type text_alternative: string
261 | @type images: list of strings
262 | @type css: string
263 | @type cc: list
264 | @type bcc: list
265 |
266 | @param mail_to: a list of valid email addresses
267 | @param mail_from: a valid sender email address.
268 | @param reply_to: a valid email address for the (potential) replies.
269 | @param html_message: the actual payload, body of the mail
270 | @param text_alternative: plain-text version of the mail body
271 | @param images: the images that are referenced in the HTML body. These should be available as files on the
272 | filesystem in the directory where the script runs. Caveat: assume jpeg image type.
273 | @param css: CSS definitions
274 | @param cc: a list of valid CC email addresses
275 | @param bcc: a list of valid BCC email addresses
276 | """
277 |
278 | if isinstance(mail_to, str):
279 | logging.warning("Deprecated, mail_to passed as string. Should be list of strings.")
280 | mail_to = [mail_to]
281 |
282 | logging.info("Sending mail [%s] to %s.", mail_subject, mail_to)
283 |
284 | # Create message container - the correct MIME type is multipart/alternative.
285 | msg_root = MIMEMultipart("alternative")
286 | msg_root["Subject"] = mail_subject
287 | msg_root["From"] = mail_from
288 | msg_root["To"] = ",".join(mail_to)
289 |
290 | if cc:
291 | logging.info("Sending mail [%s] in CC to %s.", mail_subject, cc)
292 | msg_root["Cc"] = cc
293 | mail_to.extend(cc)
294 |
295 | if bcc:
296 | logging.info("Sending mail [%s] in BCC to %s.", mail_subject, bcc)
297 | # do *not* set msg_root['Bcc'] to avoid leaking the BCC email address to all recipients
298 | mail_to.extend(bcc)
299 |
300 | if reply_to is None:
301 | reply_to = mail_from
302 | msg_root["Reply-to"] = reply_to
303 |
304 | msg_root.preamble = (
305 | "This is a multi-part message in MIME format. If your email client does not support this"
306 | "(correctly), the first part is the plain text version."
307 | )
308 |
309 | # Create the body of the message (a plain-text and an HTML version).
310 | if images is not None:
311 | html_message = self._replace_images_cid(html_message, images)
312 |
313 | # Record the MIME types of both parts - text/plain and text/html_message.
314 | msg_plain = MIMEText(text_alternative, "plain", "utf-8")
315 | msg_html = MIMEText(html_message, "html_message", "utf-8")
316 |
317 | # Attach parts into message container.
318 | # According to RFC 2046, the last part of a multipart message, in this case
319 | # the HTML message, is best and preferred.
320 | msg_root.attach(msg_plain)
321 | msg_alt = MIMEMultipart("related")
322 | msg_alt.attach(msg_html)
323 |
324 | if css is not None:
325 | msg_html_css = MIMEText(css, "css", "utf-8")
326 | msg_html_css.add_header("Content-ID", "")
327 | msg_alt.attach(msg_html_css)
328 |
329 | if images is not None:
330 | for im in images:
331 | with open(im) as image_fp:
332 | msg_image = MIMEImage(image_fp.read(), "jpeg") # FIXME: for now, we assume jpegs
333 | msg_image.add_header("Content-ID", f"<{im}>")
334 | msg_alt.attach(msg_image)
335 |
336 | msg_root.attach(msg_alt)
337 |
338 | self._send(mail_from, mail_to, mail_subject, msg_root)
339 |
--------------------------------------------------------------------------------
/lib/vsc/utils/rest.py:
--------------------------------------------------------------------------------
1 | ### External compatible license
2 | #
3 | # This file is part of agithub
4 | # Originally created by Jonathan Paugh
5 | #
6 | # https://github.com/jpaugh/agithub
7 | #
8 | # Copyright 2012 Jonathan Paugh
9 | #
10 | # Permission is hereby granted, free of charge, to any person obtaining a copy
11 | # of this software and associated documentation files (the "Software"), to deal
12 | # in the Software without restriction, including without limitation the rights
13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | # copies of the Software, and to permit persons to whom the Software is
15 | # furnished to do so, subject to the following conditions:
16 | #
17 | # The above copyright notice and this permission notice shall be included in
18 | # all copies or substantial portions of the Software.
19 | #
20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26 | # THE SOFTWARE.
27 | #
28 | """
29 | This module contains Rest api utilities,
30 | Mainly the RestClient, which you can use to easily pythonify a rest api.
31 |
32 | based on https://github.com/jpaugh/agithub/commit/1e2575825b165c1cb7cbd85c22e2561fc4d434d3
33 |
34 | @author: Jonathan Paugh
35 | @author: Jens Timmerman (Ghent University)
36 | @author: Kenneth Hoste (Ghent University)
37 | """
38 |
39 | import base64
40 | import copy
41 | import json
42 | import logging
43 | from functools import partial
44 | from urllib.parse import urlencode
45 | from urllib.request import Request, HTTPSHandler, build_opener
46 |
47 | CENSORED_MESSAGE = ""
48 |
49 |
50 | class Client:
51 | """An implementation of a REST client"""
52 |
53 | DELETE = "DELETE"
54 | GET = "GET"
55 | HEAD = "HEAD"
56 | PATCH = "PATCH"
57 | POST = "POST"
58 | PUT = "PUT"
59 |
60 | HTTP_METHODS = (
61 | DELETE,
62 | GET,
63 | HEAD,
64 | PATCH,
65 | POST,
66 | PUT,
67 | )
68 |
69 | USER_AGENT = "vsc-rest-client"
70 |
71 | def __init__(
72 | self,
73 | url,
74 | username=None,
75 | password=None,
76 | token=None,
77 | token_type="Token",
78 | user_agent=None,
79 | append_slash=False,
80 | decode=True,
81 | ):
82 | """
83 | Create a Client object,
84 | this client can consume a REST api hosted at host/endpoint
85 |
86 | If a username is given a password or a token is required.
87 | You can not use a password and a token.
88 | token_type is the typoe fo th the authorization token text in the http authentication header, defaults to Token
89 | This should be set to 'Bearer' for certain OAuth implementations.
90 | """
91 | self.auth_header = None
92 | self.username = username
93 | self.url = url
94 | self.append_slash = append_slash
95 | self.decode = decode
96 |
97 | if not user_agent:
98 | self.user_agent = self.USER_AGENT
99 | else:
100 | self.user_agent = user_agent
101 |
102 | handler = HTTPSHandler()
103 | self.opener = build_opener(handler)
104 |
105 | if username is not None:
106 | if password is None and token is None:
107 | raise TypeError("You need a password or an OAuth token to authenticate as " + username)
108 | if password is not None and token is not None:
109 | raise TypeError("You cannot use both password and OAuth token authenication")
110 |
111 | if password is not None:
112 | self.auth_header = self.hash_pass(password, username)
113 | elif token is not None:
114 | self.auth_header = f"{token_type} {token}"
115 |
116 | def _append_slash_to(self, url):
117 | """Append slash to specified URL, if desired and needed."""
118 | if self.append_slash and not url.endswith("/"):
119 | url += "/"
120 | return url
121 |
122 | def get(self, url, headers=None, **params):
123 | """
124 | Do a http get request on the given url with given headers and parameters
125 | Parameters is a dictionary that will will be urlencoded
126 | """
127 | url = self._append_slash_to(url) + self.urlencode(params)
128 | return self.request(self.GET, url, None, headers)
129 |
130 | def head(self, url, headers=None, **params):
131 | """
132 | Do a http head request on the given url with given headers and parameters
133 | Parameters is a dictionary that will will be urlencoded
134 | """
135 | url = self._append_slash_to(url) + self.urlencode(params)
136 | return self.request(self.HEAD, url, None, headers)
137 |
138 | def delete(self, url, headers=None, body=None, **params):
139 | """
140 | Do a http delete request on the given url with given headers, body and parameters
141 | Parameters is a dictionary that will will be urlencoded
142 | """
143 | url = self._append_slash_to(url) + self.urlencode(params)
144 | return self.request(self.DELETE, url, body, headers, content_type="application/json")
145 |
146 | def post(self, url, body=None, headers=None, **params):
147 | """
148 | Do a http post request on the given url with given body, headers and parameters
149 | Parameters is a dictionary that will will be urlencoded
150 | """
151 | url = self._append_slash_to(url) + self.urlencode(params)
152 | return self.request(self.POST, url, body, headers, content_type="application/json")
153 |
154 | def put(self, url, body=None, headers=None, **params):
155 | """
156 | Do a http put request on the given url with given body, headers and parameters
157 | Parameters is a dictionary that will will be urlencoded
158 | """
159 | url = self._append_slash_to(url) + self.urlencode(params)
160 | return self.request(self.PUT, url, body, headers, content_type="application/json")
161 |
162 | def patch(self, url, body=None, headers=None, **params):
163 | """
164 | Do a http patch request on the given url with given body, headers and parameters
165 | Parameters is a dictionary that will will be urlencoded
166 | """
167 | url = self._append_slash_to(url) + self.urlencode(params)
168 | return self.request(self.PATCH, url, body, headers, content_type="application/json")
169 |
170 | def request(self, method, url, body, headers, content_type=None):
171 | """Low-level networking. All HTTP-method methods call this"""
172 | # format headers
173 | if headers is None:
174 | headers = {}
175 |
176 | if content_type is not None:
177 | headers["Content-Type"] = content_type
178 |
179 | if self.auth_header is not None:
180 | headers["Authorization"] = self.auth_header
181 | headers["User-Agent"] = self.user_agent
182 |
183 | # censor contents of 'Authorization' part of header, to avoid leaking tokens or passwords in logs
184 | secret_items = ["Authorization", "X-Auth-Token"]
185 | headers_censored = self.censor_request(secret_items, headers)
186 |
187 | body_censored = body
188 | if body is not None:
189 | if isinstance(body, str):
190 | # assume serialized bodies are already clear of secrets
191 | logging.debug("Request with pre-serialized body, will not censor secrets")
192 | else:
193 | # censor contents of body to avoid leaking passwords
194 | secret_items = ["password"]
195 | body_censored = self.censor_request(secret_items, body)
196 | # serialize body in all cases
197 | body = json.dumps(body)
198 |
199 | logging.debug("cli request: %s, %s, %s, %s", method, url, body_censored, headers_censored)
200 |
201 | with self.get_connection(method, url, body, headers) as conn:
202 | status = conn.code
203 | if method == self.HEAD:
204 | pybody = conn.headers
205 | else:
206 | body = conn.read()
207 | body = body.decode("utf-8") # byte encoded response
208 | if self.decode:
209 | try:
210 | pybody = json.loads(body)
211 | except ValueError:
212 | pybody = body
213 | else:
214 | pybody = body
215 | logging.debug("reponse len: %s ", len(pybody))
216 | return status, pybody
217 |
218 | @staticmethod
219 | def censor_request(secrets, payload):
220 | """
221 | Replace secrets in payload with a censored message
222 |
223 | @type secrets: list of keys that will be censored
224 | @type payload: dictionary with headers or body of request
225 | """
226 | payload_censored = copy.deepcopy(payload)
227 |
228 | try:
229 | for secret in set(payload_censored).intersection(secrets):
230 | payload_censored[secret] = CENSORED_MESSAGE
231 | except TypeError:
232 | # Unknown payload structure, cannot censor secrets
233 | pass
234 |
235 | return payload_censored
236 |
237 | def urlencode(self, params):
238 | if not params:
239 | return ""
240 | return "?" + urlencode(params)
241 |
242 | def hash_pass(self, password, username=None):
243 | if not username:
244 | username = self.username
245 |
246 | credentials = f"{username}:{password}"
247 | credentials = credentials.encode("utf-8")
248 | encoded_credentials = base64.b64encode(credentials).strip()
249 | encoded_credentials = str(encoded_credentials, "utf-8")
250 |
251 | return "Basic " + encoded_credentials
252 |
253 | def get_connection(self, method, url, body, headers):
254 | if not self.url.endswith("/") and not url.startswith("/"):
255 | sep = "/"
256 | else:
257 | sep = ""
258 | if body is not None:
259 | body = body.encode()
260 | request = Request(self.url + sep + url, data=body)
261 | for header, value in headers.items():
262 | request.add_header(header, value)
263 | request.get_method = lambda: method
264 | logging.debug("opening request: %s%s%s", self.url, sep, url)
265 | connection = self.opener.open(request)
266 | return connection
267 |
268 |
269 | class RequestBuilder:
270 | """RequestBuilder(client).path.to.resource.method(...)
271 | stands for
272 | RequestBuilder(client).client.method('path/to/resource, ...)
273 |
274 | Also, if you use an invalid path, too bad. Just be ready to catch a
275 | You can use item access instead of attribute access. This is
276 | convenient for using variables' values and required for numbers.
277 | bad status from github.com. (Or maybe an httplib.error...)
278 |
279 | To understand the method(...) calls, check out github.client.Client.
280 | """
281 |
282 | def __init__(self, client):
283 | """Constructor"""
284 | self.client = client
285 | self.url = ""
286 |
287 | def __getattr__(self, key):
288 | """
289 | Overwrite __getattr__ to build up the equest url
290 | this enables us to do bla.some.path['something']
291 | and get the url bla/some/path/something
292 | """
293 | # make sure key is a string
294 | key = str(key)
295 | # our methods are lowercase, but our HTTP_METHOD constants are upercase, so check if it is in there, but only
296 | # if it was a lowercase key
297 | # this is here so bla.something.get() should work, and not result in bla/something/get being returned
298 | if key.upper() in self.client.HTTP_METHODS and [x for x in key if x.islower()]:
299 | mfun = getattr(self.client, key)
300 | fun = partial(mfun, url=self.url)
301 | return fun
302 | self.url += "/" + key
303 | return self
304 |
305 | __getitem__ = __getattr__
306 |
307 | def __str__(self):
308 | """If you ever stringify this, you've (probably) messed up
309 | somewhere. So let's give a semi-helpful message.
310 | """
311 | return f"I don't know about {self.url}, You probably want to do a get or other http request, use .get()"
312 |
313 | def __repr__(self):
314 | return f"{self.__class__}: {self.url}"
315 |
316 |
317 | class RestClient:
318 | """
319 | A client with a request builder, so you can easily create rest requests
320 | e.g. to create a github Rest API client just do
321 | >>> g = RestClient("https://api.github.com", username="user", password="pass")
322 | >>> g = RestClient("https://api.github.com", token="oauth token")
323 | >>> status, data = g.issues.get(filter="subscribed")
324 | >>> data
325 | ... [list_, of, stuff]
326 | >>> status, data = g.repos.jpaugh64.repla.issues[1].get()
327 | >>> data
328 | ... {
329 | ... "dict": "my issue data",
330 | ... }
331 | >>> name, repo = "jpaugh64", "repla"
332 | >>> status, data = g.repos[name][repo].issues[1].get()
333 | ... same thing
334 | >>> status, data = g.funny.I.donna.remember.that.one.get()
335 | >>> status
336 | ... 404
337 |
338 | That's all there is to it. (blah.post() should work, too.)
339 |
340 | NOTE: It is up to you to spell things correctly. Github doesn't even
341 | try to validate the url you feed it. On the other hand, it
342 | automatically supports the full API--so why should you care?
343 | """
344 |
345 | def __init__(self, *args, **kwargs):
346 | """We create a client with the given arguments"""
347 | self.client = Client(*args, **kwargs)
348 |
349 | def __getattr__(self, key):
350 | """Get an attribute, we will build a request with it"""
351 | return RequestBuilder(self.client).__getattr__(key)
352 |
--------------------------------------------------------------------------------
/lib/vsc/utils/missing.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2012-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Various functions that are missing from the default Python library.
28 |
29 | - nub(list): keep the unique elements in the list
30 | - nub_by(list, predicate): keep the unique elements (first come, first served) that do not satisfy a given predicate
31 | - find_sublist_index(list, sublist): find the index of the first
32 | occurence of the sublist in the list
33 | - Monoid: implementation of the monoid concept
34 | - MonoidDict: dictionary that combines values upon insertiong
35 | according to the given monoid
36 | - RUDict: dictionary that allows recursively updating its values (if they are dicts too) with a new RUDict
37 | - shell_quote / shell_unquote : convenience functions to quote / unquote strings in shell context
38 |
39 | @author: Andy Georges (Ghent University)
40 | @author: Stijn De Weirdt (Ghent University)
41 | """
42 |
43 | import logging
44 | import shlex
45 | import time
46 | from collections import namedtuple
47 | from functools import reduce
48 | from collections.abc import Mapping
49 |
50 |
51 | from vsc.utils.frozendict import FrozenDict
52 |
53 |
54 | def nub(list_):
55 | """Returns the unique items of a list of hashables, while preserving order of
56 | the original list, i.e. the first unique element encoutered is
57 | retained.
58 |
59 | Code is taken from
60 | http://stackoverflow.com/questions/480214/how-do-you-remove-duplicates-from-a-list-in-python-whilst-preserving-order
61 |
62 | Supposedly, this is one of the fastest ways to determine the
63 | unique elements of a list.
64 |
65 | @type list_: a list :-)
66 |
67 | @returns: a new list with each element from `list` appearing only once (cfr. Michelle Dubois).
68 | """
69 | seen = set()
70 | seen_add = seen.add
71 | return [x for x in list_ if x not in seen and not seen_add(x)]
72 |
73 |
74 | def nub_by(list_, predicate):
75 | """Returns the elements of a list that fullfil the predicate.
76 |
77 | For any pair of elements in the resulting list, the predicate does not hold. For example, the nub above
78 | can be expressed as nub_by(list, lambda x, y: x == y).
79 |
80 | @type list_: a list of items of some type t
81 | @type predicate: a function that takes two elements of type t and returns a bool
82 |
83 | @returns: the nubbed list
84 | """
85 | seen = set()
86 | seen_add = seen.add
87 | return [x for x in list_ if not any([predicate(x, y) for y in seen]) and not seen_add(x)]
88 |
89 |
90 | def find_sublist_index(ls, sub_ls):
91 | """Find the index at which the sublist sub_ls can be found in ls.
92 |
93 | @type ls: list
94 | @type sub_ls: list
95 |
96 | @return: index of the matching location or None if no match can be made.
97 | """
98 | sub_length = len(sub_ls)
99 | for i in range(len(ls)):
100 | if ls[i : (i + sub_length)] == sub_ls:
101 | return i
102 |
103 | return None
104 |
105 |
106 | class Monoid:
107 | """A monoid is a mathematical object with a default element (mempty or null) and a default operation to combine
108 | two elements of a given data type.
109 |
110 | Taken from http://fmota.eu/2011/10/09/monoids-in-python.html under the do whatever you want license.
111 | """
112 |
113 | def __init__(self, null, mappend):
114 | """Initialise.
115 |
116 | @type null: default element of some data type, e.g., [] for list or 0 for int
117 | (identity element in an Abelian group)
118 | @type op: mappend operation to combine two elements of the target datatype
119 | """
120 | self.null = null
121 | self.mappend = mappend
122 |
123 | def fold(self, xs):
124 | """fold over the elements of the list, combining them into a single element of the target datatype."""
125 | if hasattr(xs, "__fold__"):
126 | return xs.__fold__(self)
127 | else:
128 | return reduce(self.mappend, xs, self.null)
129 |
130 | def __call__(self, *args):
131 | """When the monoid is called, the values are folded over and the resulting value is returned."""
132 | return self.fold(args)
133 |
134 | def star(self):
135 | """Return a new similar monoid."""
136 | return Monoid(self.null, self.mappend)
137 |
138 |
139 | class MonoidDict(dict):
140 | """A dictionary with a monoid operation, that allows combining values in the dictionary according to the mappend
141 | operation in the monoid.
142 | """
143 |
144 | def __init__(self, monoid, *args, **kwargs):
145 | """Initialise.
146 |
147 | @type monoid: Monoid instance
148 | """
149 | super().__init__(*args, **kwargs)
150 | self.monoid = monoid
151 |
152 | def __setitem__(self, key, value):
153 | """Combine the value the dict has for the key with the new value using the mappend operation."""
154 | if super().__contains__(key):
155 | current = super().__getitem__(key)
156 | super().__setitem__(key, self.monoid(current, value))
157 | else:
158 | super().__setitem__(key, value)
159 |
160 | def __getitem__(self, key):
161 | """Obtain the dictionary value for the given key. If no value is present,
162 | we return the monoid's mempty (null).
163 | """
164 | if not super().__contains__(key):
165 | return self.monoid.null
166 | else:
167 | return super().__getitem__(key)
168 |
169 |
170 | class RUDict(dict):
171 | """Recursively updatable dictionary.
172 |
173 | When merging with another dictionary (of the same structure), it will keep
174 | updating the values as well if they are dicts or lists.
175 |
176 | Code taken from http://stackoverflow.com/questions/6256183/combine-two-dictionaries-of-dictionaries-python.
177 | """
178 |
179 | def update(self, E=None, **F):
180 | if E is not None:
181 | if "keys" in dir(E) and callable(getattr(E, "keys")):
182 | for k in E:
183 | if k in self: # existing ...must recurse into both sides
184 | self.r_update(k, E)
185 | else: # doesn't currently exist, just update
186 | self[k] = E[k]
187 | else:
188 | for k, v in E:
189 | self.r_update(k, {k: v})
190 |
191 | for k, v in F.items():
192 | self.r_update(k, {k: v})
193 |
194 | def r_update(self, key, other_dict):
195 | """Recursive update."""
196 | if isinstance(self[key], dict) and isinstance(other_dict[key], dict):
197 | od = RUDict(self[key])
198 | nd = other_dict[key]
199 | od.update(nd)
200 | self[key] = od
201 | elif isinstance(self[key], list):
202 | if isinstance(other_dict[key], list):
203 | self[key].extend(other_dict[key])
204 | else:
205 | self[key] = self[key].append(other_dict[key])
206 | else:
207 | self[key] = other_dict[key]
208 |
209 |
210 | class FrozenDictKnownKeys(FrozenDict):
211 | """A frozen dictionary only allowing known keys."""
212 |
213 | # list of known keys
214 | KNOWN_KEYS = []
215 |
216 | def __init__(self, *args, **kwargs):
217 | """Constructor, only way to define the contents."""
218 | # support ignoring of unknown keys
219 | ignore_unknown_keys = kwargs.pop("ignore_unknown_keys", False)
220 |
221 | # handle unknown keys: either ignore them or raise an exception
222 | tmpdict = dict(*args, **kwargs)
223 | unknown_keys = [key for key in tmpdict if key not in self.KNOWN_KEYS]
224 | if unknown_keys:
225 | if ignore_unknown_keys:
226 | for key in unknown_keys:
227 | logging.debug("Ignoring unknown key '%s' (value '%s')", key, args[0][key])
228 | # filter key out of dictionary before creating instance
229 | del tmpdict[key]
230 | else:
231 | logging.error("Encountered unknown keys %s (known keys: %s)", unknown_keys, self.KNOWN_KEYS)
232 | raise KeyError("Encountered unknown keys")
233 |
234 | super().__init__(tmpdict)
235 |
236 | # pylint: disable=arguments-differ
237 | def __getitem__(self, key, *args, **kwargs):
238 | """Redefine __getitem__ to provide a better KeyError message."""
239 | try:
240 | return super().__getitem__(key, *args, **kwargs)
241 | except KeyError as err:
242 | if key in self.KNOWN_KEYS:
243 | raise KeyError(err)
244 | else:
245 | raise KeyError(
246 | f"Unknown key '{key}' for {self.__class__.__name__} instance (known keys: {self.KNOWN_KEYS})"
247 | )
248 |
249 |
250 | def shell_quote(x):
251 | """Add quotes so it can be passed to shell"""
252 | return shlex.quote(str(x))
253 |
254 |
255 | def shell_unquote(x):
256 | """Take a literal string, remove the quotes as if it were passed by shell"""
257 | # it expects a string
258 | return " ".join(shlex.split(str(x)))
259 |
260 |
261 | def get_class_for(modulepath, class_name):
262 | """
263 | Get class for a given Python class name and Python module path.
264 |
265 | @param modulepath: Python module path (e.g., 'vsc.utils.generaloption')
266 | @param class_name: Python class name (e.g., 'GeneralOption')
267 | """
268 | # try to import specified module path, reraise ImportError if it occurs
269 | try:
270 | module = __import__(modulepath, globals(), locals(), [""])
271 | except ImportError as err:
272 | raise ImportError(err)
273 | # try to import specified class name from specified module path, throw ImportError if this fails
274 | try:
275 | klass = getattr(module, class_name)
276 | except AttributeError as err:
277 | raise ImportError(f"Failed to import {class_name} from {modulepath}: {err}")
278 | return klass
279 |
280 |
281 | def get_subclasses_dict(klass, include_base_class=False):
282 | """Get dict with subclasses per classes, recursively from the specified base class."""
283 | res = {}
284 | subclasses = klass.__subclasses__()
285 | if include_base_class:
286 | res.update({klass: subclasses})
287 | for subclass in subclasses:
288 | # always include base class for recursive call
289 | res.update(get_subclasses_dict(subclass, include_base_class=True))
290 | return res
291 |
292 |
293 | def get_subclasses(klass, include_base_class=False):
294 | """Get list of all subclasses, recursively from the specified base class."""
295 | return get_subclasses_dict(klass, include_base_class=include_base_class).keys()
296 |
297 |
298 | class TryOrFail:
299 | """
300 | Perform the function n times, catching each exception in the exception tuple except on the last try
301 | where it will be raised again.
302 | """
303 |
304 | def __init__(self, n, exceptions=(Exception,), sleep=0):
305 | self.n = n
306 | self.exceptions = exceptions
307 | self.sleep = sleep
308 |
309 | def __call__(self, function):
310 | def new_function(*args, **kwargs):
311 | for i in range(0, self.n):
312 | try:
313 | return function(*args, **kwargs)
314 | except self.exceptions as err:
315 | if i == self.n - 1:
316 | raise
317 | logging.exception("try_or_fail caught an exception - attempt %d: %s", i, err)
318 | if self.sleep > 0:
319 | logging.warning("try_or_fail is sleeping for %d seconds before the next attempt", self.sleep)
320 | time.sleep(self.sleep)
321 |
322 | return None
323 |
324 | return new_function
325 |
326 |
327 | def post_order(graph, root):
328 | """
329 | Walk the graph from the given root in a post-order manner by providing the corresponding generator
330 | """
331 | for node in graph[root]:
332 | yield from post_order(graph, node)
333 | yield root
334 |
335 |
336 | def topological_sort(graph):
337 | """
338 | Perform topological sorting of the given graph.
339 |
340 | The graph is a dict with the values for a key being the dependencies, i.e., an arrow from key to each value.
341 | """
342 | visited = set()
343 | for root in graph:
344 | for node in post_order(graph, root):
345 | if node not in visited:
346 | yield node
347 | visited.add(node)
348 |
349 |
350 | # https://stackoverflow.com/questions/11351032/namedtuple-and-default-values-for-optional-keyword-arguments
351 | def namedtuple_with_defaults(typename, field_names, default_values=()):
352 | T = namedtuple(typename, field_names)
353 | T.__new__.__defaults__ = (None,) * len(T._fields)
354 | if isinstance(default_values, Mapping):
355 | prototype = T(**default_values)
356 | else:
357 | prototype = T(*default_values)
358 | T.__new__.__defaults__ = tuple(prototype)
359 | return T
360 |
361 |
362 | def ensure_ascii_string(value):
363 | """
364 | Convert the provided value to an ASCII string (no Unicode characters).
365 | """
366 | if isinstance(value, bytes):
367 | # if we have a bytestring, decode it to a regular string using ASCII encoding,
368 | # and replace Unicode characters with backslash escaped sequences
369 | value = value.decode("ascii", "backslashreplace")
370 | else:
371 | # for other values, just convert to a string (which may still include Unicode characters)
372 | # then convert to bytestring with UTF-8 encoding,
373 | # which can then be decoded to regular string using ASCII encoding
374 | value = bytes(str(value), encoding="utf-8").decode(encoding="ascii", errors="backslashreplace")
375 |
376 | return value
377 |
--------------------------------------------------------------------------------
/test/run.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2012-2025 Ghent University
3 | #
4 | # This file is part of vsc-base,
5 | # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6 | # with support of Ghent University (http://ugent.be/hpc),
7 | # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8 | # the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9 | # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10 | #
11 | # https://github.com/hpcugent/vsc-base
12 | #
13 | # vsc-base is free software: you can redistribute it and/or modify
14 | # it under the terms of the GNU Library General Public License as
15 | # published by the Free Software Foundation, either version 2 of
16 | # the License, or (at your option) any later version.
17 | #
18 | # vsc-base is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | # GNU Library General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Library General Public License
24 | # along with vsc-base. If not, see .
25 | #
26 | """
27 | Tests for the vsc.utils.run module.
28 |
29 | @author: Stijn De Weirdt (Ghent University)
30 | @author: Kenneth Hoste (Ghent University)
31 | """
32 | import os
33 | import re
34 | import sys
35 | import tempfile
36 | import time
37 | import shutil
38 |
39 | # Uncomment when debugging, cannot enable permanetnly, messes up tests that toggle debugging
40 | #logging.basicConfig(level=logging.DEBUG)
41 |
42 | from vsc.utils.missing import shell_quote
43 | from vsc.utils.run import (
44 | CmdList, run, run_simple, asyncloop, run_asyncloop,
45 | run_timeout, RunTimeout,
46 | RunQA, RunNoShellQA,
47 | async_to_stdout, run_async_to_stdout,
48 | )
49 | from vsc.utils.run import RUNRUN_TIMEOUT_OUTPUT, RUNRUN_TIMEOUT_EXITCODE, RUNRUN_QA_MAX_MISS_EXITCODE
50 | from vsc.install.testing import TestCase
51 |
52 |
53 | SCRIPTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'runtests')
54 | SCRIPT_SIMPLE = os.path.join(SCRIPTS_DIR, 'simple.py')
55 | SCRIPT_QA = os.path.join(SCRIPTS_DIR, 'qa.py')
56 | SCRIPT_NESTED = os.path.join(SCRIPTS_DIR, 'run_nested.sh')
57 |
58 |
59 | TEST_GLOB = ['ls','test/sandbox/testpkg/*']
60 |
61 | class RunQAShort(RunNoShellQA):
62 | LOOP_MAX_MISS_COUNT = 3 # approx 3 sec
63 |
64 | class RunLegQAShort(RunQA):
65 | LOOP_MAX_MISS_COUNT = 3 # approx 3 sec
66 |
67 | run_qas = RunQAShort.run
68 | run_legacy_qas = RunLegQAShort.run
69 |
70 |
71 | class TestRun(TestCase):
72 | """Test for the run module."""
73 |
74 | def setUp(self):
75 | super().setUp()
76 | self.tempdir = tempfile.mkdtemp()
77 |
78 | def tearDown(self):
79 | super().tearDown()
80 | shutil.rmtree(self.tempdir)
81 |
82 | def glob_output(self, output, ec=None):
83 | """Verify that output of TEST_GLOB is ok"""
84 | self.assertTrue(all(x in output.lower() for x in ['__init__.py', 'testmodule.py', 'testmodulebis.py']))
85 | if ec is not None:
86 | self.assertEqual(ec, 0)
87 |
88 | def test_simple(self):
89 | ec, output = run_simple([sys.executable, SCRIPT_SIMPLE, 'shortsleep'])
90 | self.assertEqual(ec, 0)
91 | self.assertTrue('shortsleep' in output.lower())
92 |
93 | def test_startpath(self):
94 | cwd = os.getcwd()
95 |
96 | cmd = "echo foo > bar"
97 |
98 | self.assertErrorRegex(Exception, '.*', run_simple, cmd, startpath='/no/such/directory')
99 |
100 | ec, out = run_simple(cmd, startpath=self.tempdir)
101 |
102 | # successfull command, no output
103 | self.assertEqual(ec, 0)
104 | self.assertEqual(out, '')
105 |
106 | # command was actually executed, in specified directory
107 | self.assertTrue(os.path.exists(os.path.join(self.tempdir, 'bar')))
108 | with open(os.path.join(self.tempdir, 'bar')) as fih:
109 | self.assertEqual(fih.read(), 'foo\n')
110 |
111 | # we should still be in directory we were in originally
112 | self.assertEqual(cwd, os.getcwd())
113 |
114 | def test_simple_asyncloop(self):
115 | ec, output = run_asyncloop([sys.executable, SCRIPT_SIMPLE, 'shortsleep'])
116 | self.assertEqual(ec, 0)
117 | self.assertTrue('shortsleep' in output.lower())
118 |
119 | def test_simple_ns_asyncloop(self):
120 | ec, output = asyncloop([sys.executable, SCRIPT_SIMPLE, 'shortsleep'])
121 | self.assertEqual(ec, 0)
122 | self.assertTrue('shortsleep' in output.lower())
123 |
124 | def test_simple_glob(self):
125 | ec, output = run_simple(" ".join(TEST_GLOB))
126 | self.glob_output(output, ec=ec)
127 | ec, output = run_simple(TEST_GLOB)
128 | self.glob_output(output, ec=ec)
129 |
130 | def test_noshell_glob(self):
131 | ec, output = run('ls test/sandbox/testpkg/*')
132 | self.assertTrue(ec > 0)
133 | regex = re.compile(r"'?test/sandbox/testpkg/\*'?: No such file or directory")
134 | self.assertTrue(regex.search(output), f"Pattern '{regex.pattern}' found in: {output}")
135 | ec, output = run_simple(TEST_GLOB)
136 | self.glob_output(output, ec=ec)
137 |
138 | def test_noshell_async_stdout_glob(self):
139 |
140 | for msg in ['ok', "message with spaces", 'this -> ¢ <- is unicode']:
141 |
142 | self.mock_stderr(True)
143 | self.mock_stdout(True)
144 | ec, output = async_to_stdout(f"/bin/echo {msg}")
145 | stderr, stdout = self.get_stderr(), self.get_stdout()
146 | self.mock_stderr(False)
147 | self.mock_stdout(False)
148 |
149 | # there should be no output to stderr
150 | self.assertFalse(stderr)
151 |
152 | msg = msg.replace('¢', '\\xc2\\xa2')
153 |
154 | self.assertEqual(ec, 0)
155 | self.assertEqual(output, msg + '\n')
156 | self.assertEqual(output, stdout)
157 |
158 | def test_noshell_async_stdout_stdin(self):
159 |
160 | # test with both regular strings and bytestrings (only matters w.r.t. Python 3 compatibility)
161 | inputs = [
162 | 'foo',
163 | b'foo',
164 | "testing, 1, 2, 3",
165 | b"testing, 1, 2, 3",
166 | ]
167 | for inp in inputs:
168 | self.mock_stderr(True)
169 | self.mock_stdout(True)
170 | ec, output = async_to_stdout('/bin/cat', input=inp)
171 | stderr, stdout = self.get_stderr(), self.get_stdout()
172 | self.mock_stderr(False)
173 | self.mock_stdout(False)
174 |
175 | # there should be no output to stderr
176 | self.assertFalse(stderr)
177 |
178 | # output is always string, not bytestring, so convert before comparison
179 | # (only needed for Python 3)
180 | if not isinstance(inp, str):
181 | inp = inp.decode(encoding='utf-8')
182 |
183 | self.assertEqual(ec, 0)
184 | self.assertEqual(output, inp)
185 | self.assertEqual(output, stdout)
186 |
187 | def test_async_stdout_glob(self):
188 | self.mock_stdout(True)
189 | ec, output = run_async_to_stdout("/bin/echo ok")
190 | self.assertEqual(ec, 0)
191 | self.assertEqual(output, "ok\n", "returned run_async_to_stdout output is as expected")
192 | self.assertEqual(sys.stdout.getvalue(), output, "returned output is send to stdout")
193 |
194 | def test_noshell_executable(self):
195 | ec, output = run("echo '(foo bar)'")
196 | self.assertEqual(ec, 0)
197 | self.assertTrue('(foo bar)' in output)
198 |
199 | ec, output = run(['echo', "(foo bar)"])
200 | self.assertEqual(ec, 0)
201 | self.assertTrue('(foo bar)' in output)
202 |
203 | # to run Python command, it's required to use the right executable (Python shell rather than default)
204 | python_cmd = shell_quote(sys.executable)
205 | ec, output = run(f"""{python_cmd} -c 'print ("foo")'""")
206 | self.assertEqual(ec, 0)
207 | self.assertTrue('foo' in output)
208 |
209 | ec, output = run([sys.executable, '-c', 'print ("foo")'])
210 | self.assertEqual(ec, 0)
211 | self.assertTrue('foo' in output)
212 |
213 | def test_timeout(self):
214 | timeout = 3
215 |
216 | # longsleep is 10sec
217 | start = time.time()
218 | ec, output = run_timeout([sys.executable, SCRIPT_SIMPLE, 'longsleep'], timeout=timeout)
219 | stop = time.time()
220 | self.assertEqual(ec, RUNRUN_TIMEOUT_EXITCODE, msg='longsleep stopped due to timeout')
221 | self.assertEqual(RUNRUN_TIMEOUT_OUTPUT, output, msg='longsleep expected output')
222 | self.assertTrue(stop - start < timeout + 1, msg='longsleep timeout within margin') # give 1 sec margin
223 |
224 | # run_nested is 15 seconds sleep
225 | # 1st arg depth: 2 recursive starts
226 | # 2nd arg file: output file: format 'depth pid' (only other processes are sleep)
227 |
228 | def check_pid(pid):
229 | """ Check For the existence of a unix pid. """
230 | try:
231 | os.kill(pid, 0)
232 | except OSError as err:
233 | sys.stderr.write(f"check_pid in test_timeout: {err}\n")
234 | return False
235 | else:
236 | return True
237 |
238 | default = RunTimeout.KILL_PGID
239 |
240 | def do_test(kill_pgid):
241 | depth = 2 # this is the parent
242 | res_fn = os.path.join(self.tempdir, f'nested_kill_pgid_{kill_pgid}')
243 | start = time.time()
244 | RunTimeout.KILL_PGID = kill_pgid
245 | ec, output = run_timeout([SCRIPT_NESTED, str(depth), res_fn], timeout=timeout)
246 | # reset it to default
247 | RunTimeout.KILL_PGID = default
248 | stop = time.time()
249 | self.assertEqual(ec, RUNRUN_TIMEOUT_EXITCODE, msg=f'run_nested kill_pgid {kill_pgid} stopped due to timeout')
250 | self.assertTrue(stop - start < timeout + 1, msg=f'run_nested kill_pgid {kill_pgid} timeout within margin') # give 1 sec margin
251 | # make it's not too fast
252 | time.sleep(10)
253 | # there's now 6 seconds to complete the remainder
254 | pids = list(range(depth+1))
255 | # normally this is ordered output, but you never know
256 |
257 | with open(res_fn) as fih:
258 | lines = fih.readlines()
259 | for line in lines:
260 | dep, pid, _ = line.strip().split(" ") # 3rd is PPID
261 | pids[int(dep)] = int(pid)
262 |
263 | # pids[0] should be killed
264 | self.assertFalse(check_pid(pids[depth]), f"main depth={depth} pid (pids {pids}) is killed by timeout")
265 |
266 | if kill_pgid:
267 | # others should be killed as well
268 | test_fn = self.assertFalse
269 | msg = ""
270 | else:
271 | # others should be running
272 | test_fn = self.assertTrue
273 | msg = " not"
274 |
275 | for dep, pid in enumerate(pids[:depth]):
276 | test_fn(check_pid(pid), f"depth={dep} pid (pids {pids}) is{msg} killed kill_pgid {kill_pgid}")
277 |
278 | # clean them all
279 | for pid in pids:
280 | try:
281 | os.kill(pid, 0) # test first
282 | os.kill(pid, 9)
283 | except OSError:
284 | pass
285 |
286 | do_test(False)
287 | # TODO: find a way to change the pid group before starting this test
288 | # now it kills the test process too ;)
289 | # It's ok not to test, as it is not the default, and it's not easy to change it
290 | #do_test(True)
291 |
292 | def test_qa_simple(self):
293 | """Simple testing"""
294 | ec, output = run_qas([sys.executable, SCRIPT_QA, 'noquestion'])
295 | self.assertEqual(ec, 0)
296 |
297 | qa_dict = {
298 | 'Simple question:': 'simple answer',
299 | }
300 | ec, output = run_qas([sys.executable, SCRIPT_QA, 'simple'], qa=qa_dict)
301 | self.assertEqual(ec, 0)
302 |
303 | def test_qa_regex(self):
304 | """Test regex based q and a (works only for qa_reg)"""
305 | qa_dict = {
306 | r'\s(?P\d+(?:\.\d+)?).*?What time is it\?': '%(time)s',
307 | }
308 | ec, output = run_qas([sys.executable, SCRIPT_QA, 'whattime'], qa_reg=qa_dict)
309 | self.assertEqual(ec, 0)
310 |
311 | def test_qa_legacy_regex(self):
312 | """Test regex based q and a (works only for qa_reg)"""
313 | qa_dict = {
314 | r'\s(?P\d+(?:\.\d+)?).*?What time is it\?': '%(time)s',
315 | }
316 | ec, output = run_legacy_qas([sys.executable, SCRIPT_QA, 'whattime'], qa_reg=qa_dict)
317 | self.assertEqual(ec, 0)
318 |
319 | def test_qa_noqa(self):
320 | """Test noqa"""
321 | # this has to fail
322 | qa_dict = {
323 | 'Now is the time.': 'OK',
324 | }
325 | ec, output = run_qas([sys.executable, SCRIPT_QA, 'waitforit'], qa=qa_dict)
326 | self.assertEqual(ec, RUNRUN_QA_MAX_MISS_EXITCODE)
327 |
328 | # this has to work
329 | no_qa = [r'Wait for it \(\d+ seconds\)']
330 | ec, output = run_qas([sys.executable, SCRIPT_QA, 'waitforit'], qa=qa_dict, no_qa=no_qa)
331 | self.assertEqual(ec, 0)
332 |
333 | def test_qa_list_of_answers(self):
334 | """Test qa with list of answers."""
335 | # test multiple answers in qa
336 | qa_dict = {
337 | "Enter a number ('0' to stop):": ['1', '2', '4', '0'],
338 | }
339 | ec, output = run_qas([sys.executable, SCRIPT_QA, 'ask_number', '4'], qa=qa_dict)
340 | self.assertEqual(ec, 0)
341 | answer_re = re.compile(".*Answer: 7$")
342 | self.assertTrue(answer_re.match(output), f"'{output}' matches pattern '{answer_re.pattern}'")
343 |
344 | # test multple answers in qa_reg
345 | # and test premature exit on 0 while we're at it
346 | qa_reg_dict = {
347 | r"Enter a number \(.*\):": ['2', '3', '5', '0'] + ['100'] * 100,
348 | }
349 | ec, output = run_qas([sys.executable, SCRIPT_QA, 'ask_number', '100'], qa_reg=qa_reg_dict)
350 | self.assertEqual(ec, 0)
351 | answer_re = re.compile(".*Answer: 10$")
352 | self.assertTrue(answer_re.match(output), f"'{output}' matches pattern '{answer_re.pattern}'")
353 |
354 | # verify type checking on answers
355 | self.assertErrorRegex(TypeError, "Invalid type for answer", run_qas, [], qa={'q': 1})
356 |
357 | # test more questions than answers, both with and without cycling
358 | qa_reg_dict = {
359 | r"Enter a number \(.*\):": ['2', '7'],
360 | }
361 | # loop 3 times, with cycling (the default) => 2 + 7 + 2 + 7 = 18
362 | self.assertTrue(RunQAShort.CYCLE_ANSWERS)
363 | orig_cycle_answers = RunQAShort.CYCLE_ANSWERS
364 | RunQAShort.CYCLE_ANSWERS = True
365 | ec, output = run_qas([sys.executable, SCRIPT_QA, 'ask_number', '4'], qa_reg=qa_reg_dict)
366 | self.assertEqual(ec, 0)
367 | answer_re = re.compile(".*Answer: 18$")
368 | self.assertTrue(answer_re.match(output), f"'{output}' matches pattern '{answer_re.pattern}'")
369 | # loop 3 times, no cycling => 2 + 7 + 7 + 7 = 23
370 | RunQAShort.CYCLE_ANSWERS = False
371 | ec, output = run_qas([sys.executable, SCRIPT_QA, 'ask_number', '4'], qa_reg=qa_reg_dict)
372 | self.assertEqual(ec, 0)
373 | answer_re = re.compile(".*Answer: 23$")
374 | self.assertTrue(answer_re.match(output), f"'{output}' matches pattern '{answer_re.pattern}'")
375 | # restore
376 | RunQAShort.CYCLE_ANSWERS = orig_cycle_answers
377 |
378 | def test_qa_no_newline(self):
379 | """Test we do not add newline to the answer."""
380 | qa_dict = {
381 | 'Do NOT give me a newline': 'Sure',
382 | }
383 | ec, _ = run_qas([sys.executable, SCRIPT_QA, 'nonewline'], qa=qa_dict, add_newline=False)
384 | self.assertEqual(ec, 0)
385 |
386 | def test_cmdlist(self):
387 | """Tests for CmdList."""
388 |
389 | # starting with empty command is supported
390 | # this is mainly useful when parts of a command are put together separately (cfr. mympirun)
391 | cmd = CmdList()
392 | self.assertEqual(cmd, [])
393 | cmd.add('-x')
394 | self.assertEqual(cmd, ['-x'])
395 |
396 | cmd = CmdList('test')
397 | self.assertEqual(cmd, ['test'])
398 |
399 | # can add options/arguments via string or list of strings
400 | cmd.add('-t')
401 | cmd.add(['--opt', 'foo', '-o', 'bar', '--optbis=baz'])
402 |
403 | expected = ['test', '-t', '--opt', 'foo', '-o', 'bar', '--optbis=baz']
404 | self.assertEqual(cmd, ['test', '-t', '--opt', 'foo', '-o', 'bar', '--optbis=baz'])
405 |
406 | # add options/arguments via a template
407 | cmd.add('%(name)s', tmpl_vals={'name': 'namegoeshere'})
408 | cmd.add(['%(two)s', '%(one)s'], tmpl_vals={'one': 1, 'two': 2})
409 | cmd.add('%(three)s%(five)s%(one)s', tmpl_vals={'one': '1', 'three': '3', 'five': '5'})
410 | cmd.add('%s %s %s', tmpl_vals=('foo', 'bar', 'baz'))
411 |
412 | expected.extend(['namegoeshere', '2', '1', '351', 'foo bar baz'])
413 | self.assertEqual(cmd, expected)
414 |
415 | # .append and .extend are broken, on purpose, to force use of add
416 | self.assertErrorRegex(NotImplementedError, "Use add", cmd.append, 'test')
417 | self.assertErrorRegex(NotImplementedError, "Use add", cmd.extend, ['test1', 'test2'])
418 |
419 | # occurence of spaces can be disallowed (but is allowed by default)
420 | cmd.add('this has spaces')
421 |
422 | err = "Found one or more spaces"
423 | self.assertErrorRegex(ValueError, err, cmd.add, 'this has spaces', allow_spaces=False)
424 |
425 | kwargs = {
426 | 'tmpl_vals': {'foo': 'this has spaces'},
427 | 'allow_spaces': False,
428 | }
429 | self.assertErrorRegex(ValueError, err, cmd.add, '%(foo)s', **kwargs)
430 |
431 | kwargs = {
432 | 'tmpl_vals': {'one': 'one ', 'two': 'two'},
433 | 'allow_spaces': False,
434 | }
435 | self.assertErrorRegex(ValueError, err, cmd.add, '%(one)s%(two)s', **kwargs)
436 |
437 | expected.append('this has spaces')
438 | self.assertEqual(cmd, expected)
439 |
440 | # can also init with multiple arguments
441 | cmd = CmdList('echo', "hello world", 'test')
442 | self.assertEqual(cmd, ['echo', "hello world", 'test'])
443 |
444 | # can also create via template
445 | self.assertEqual(CmdList('%(one)s', tmpl_vals={'one': 1}), ['1'])
446 |
447 | # adding non-string items yields an error
448 | self.assertErrorRegex(ValueError, "Non-string item", cmd.add, 1)
449 | self.assertErrorRegex(ValueError, "Non-string item", cmd.add, None)
450 | self.assertErrorRegex(ValueError, "Non-string item", cmd.add, ['foo', None])
451 |
452 | # starting with non-string stuff also fails
453 | self.assertErrorRegex(ValueError, "Non-string item", CmdList, 1)
454 | self.assertErrorRegex(ValueError, "Non-string item", CmdList, ['foo', None])
455 | self.assertErrorRegex(ValueError, "Non-string item", CmdList, ['foo', ['bar', 'baz']])
456 | self.assertErrorRegex(ValueError, "Found one or more spaces", CmdList, 'this has spaces', allow_spaces=False)
457 |
458 | def test_env(self):
459 | ec, output = run(cmd="/usr/bin/env", env = {"MYENVVAR": "something"})
460 | self.assertEqual(ec, 0)
461 | self.assertTrue('myenvvar=something' in output.lower())
462 |
--------------------------------------------------------------------------------
/lib/vsc/utils/optcomplete.py:
--------------------------------------------------------------------------------
1 | ### External compatible license
2 | # ******************************************************************************\
3 | # * Copyright (c) 2003-2004, Martin Blais
4 | # * All rights reserved.
5 | # *
6 | # * Redistribution and use in source and binary forms, with or without
7 | # * modification, are permitted provided that the following conditions are
8 | # * met:
9 | # *
10 | # * * Redistributions of source code must retain the above copyright
11 | # * notice, this list of conditions and the following disclaimer.
12 | # *
13 | # * * Redistributions in binary form must reproduce the above copyright
14 | # * notice, this list of conditions and the following disclaimer in the
15 | # * documentation and/or other materials provided with the distribution.
16 | # *
17 | # * * Neither the name of the Martin Blais, Furius, nor the names of its
18 | # * contributors may be used to endorse or promote products derived from
19 | # * this software without specific prior written permission.
20 | # *
21 | # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 | # * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 | # * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
24 | # * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25 | # * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26 | # * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27 | # * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28 | # * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29 | # * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30 | # * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31 | # * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 | # ******************************************************************************\
33 |
34 | """Automatic completion for optparse module.
35 |
36 | This module provide automatic bash completion support for programs that use the
37 | optparse module. The premise is that the optparse options parser specifies
38 | enough information (and more) for us to be able to generate completion strings
39 | esily. Another advantage of this over traditional completion schemes where the
40 | completion strings are hard-coded in a separate bash source file, is that the
41 | same code that parses the options is used to generate the completions, so the
42 | completions is always up-to-date with the program itself.
43 |
44 | In addition, we allow you specify a list of regular expressions or code that
45 | define what kinds of files should be proposed as completions to this file if
46 | needed. If you want to implement more complex behaviour, you can instead
47 | specify a function, which will be called with the current directory as an
48 | argument.
49 |
50 | You need to activate bash completion using the shell script function that comes
51 | with optcomplete (see http://furius.ca/optcomplete for more details).
52 |
53 | @author: Martin Blais (blais@furius.ca)
54 | @author: Stijn De Weirdt (Ghent University)
55 |
56 | This is a copy of optcomplete.py (changeset 17:e0a9131a94cc)
57 | from source: https://hg.furius.ca/public/optcomplete
58 |
59 | Modification by stdweird:
60 | - cleanup
61 | """
62 |
63 | # Bash Protocol Description
64 | # -------------------------
65 | # 'COMP_CWORD'
66 | # An index into `${COMP_WORDS}' of the word containing the current
67 | # cursor position. This variable is available only in shell
68 | # functions invoked by the programmable completion facilities (*note
69 | # Programmable Completion::).
70 | #
71 | # 'COMP_LINE'
72 | # The current command line. This variable is available only in
73 | # shell functions and external commands invoked by the programmable
74 | # completion facilities (*note Programmable Completion::).
75 | #
76 | # 'COMP_POINT'
77 | # The index of the current cursor position relative to the beginning
78 | # of the current command. If the current cursor position is at the
79 | # end of the current command, the value of this variable is equal to
80 | # `${#COMP_LINE}'. This variable is available only in shell
81 | # functions and external commands invoked by the programmable
82 | # completion facilities (*note Programmable Completion::).
83 | #
84 | # 'COMP_WORDS'
85 | # An array variable consisting of the individual words in the
86 | # current command line. This variable is available only in shell
87 | # functions invoked by the programmable completion facilities (*note
88 | # Programmable Completion::).
89 | #
90 | # 'COMPREPLY'
91 | # An array variable from which Bash reads the possible completions
92 | # generated by a shell function invoked by the programmable
93 | # completion facility (*note Programmable Completion::).
94 |
95 | import copy
96 | import glob
97 | import logging
98 | import os
99 | import re
100 | import shlex
101 | import sys
102 | import types
103 |
104 | from optparse import OptionParser, Option
105 | from pprint import pformat
106 |
107 | from vsc.utils.missing import shell_quote
108 |
109 | debugfn = None # for debugging only
110 |
111 | OPTCOMPLETE_ENVIRONMENT = "OPTPARSE_AUTO_COMPLETE"
112 |
113 | BASH = "bash"
114 |
115 | DEFAULT_SHELL = BASH
116 |
117 | SHELL = DEFAULT_SHELL
118 |
119 | OPTION_CLASS = Option
120 | OPTIONPARSER_CLASS = OptionParser
121 |
122 |
123 | def set_optionparser(option_class, optionparser_class):
124 | """Set the default Option and OptionParser class"""
125 | global OPTION_CLASS
126 | global OPTIONPARSER_CLASS
127 | OPTION_CLASS = option_class
128 | OPTIONPARSER_CLASS = optionparser_class
129 |
130 |
131 | def get_shell():
132 | """Determine the shell, update class constant SHELL and return the shell
133 | Idea is to call it just once
134 | """
135 | global SHELL
136 | SHELL = os.path.basename(os.environ.get("SHELL", DEFAULT_SHELL))
137 | return SHELL
138 |
139 |
140 | # get the shell
141 | get_shell()
142 |
143 |
144 | class CompleterMissingCallArgument(Exception):
145 | """Exception to raise when call arg is missing"""
146 |
147 |
148 | class Completer:
149 | """Base class to derive all other completer classes from.
150 | It generates an empty completion list
151 | """
152 |
153 | CALL_ARGS = None # list of named args that must be passed
154 | CALL_ARGS_OPTIONAL = None # list of named args that can be passed
155 |
156 | def __call__(self, **kwargs):
157 | """Check mandatory args, then return _call"""
158 | all_args = []
159 | if self.CALL_ARGS is not None:
160 | for arg in self.CALL_ARGS:
161 | all_args.append(arg)
162 | if arg not in kwargs:
163 | msg = f"{self.__class__.__name__} __call__ missing mandatory arg {arg}"
164 | raise CompleterMissingCallArgument(msg)
165 |
166 | if self.CALL_ARGS_OPTIONAL is not None:
167 | all_args.extend(self.CALL_ARGS_OPTIONAL)
168 |
169 | for arg in list(kwargs.keys()):
170 | if arg not in all_args:
171 | # remove it
172 | kwargs.pop(arg)
173 |
174 | return self._call(**kwargs)
175 |
176 | def _call(self, **kwargs): # pylint: disable=unused-argument
177 | """Return empty list"""
178 | return []
179 |
180 |
181 | class NoneCompleter(Completer):
182 | """Generates empty completion list. For compatibility reasons."""
183 |
184 |
185 | class ListCompleter(Completer):
186 | """Completes by filtering using a fixed list of strings."""
187 |
188 | def __init__(self, stringlist):
189 | self.olist = stringlist
190 |
191 | def _call(self, **kwargs):
192 | """Return the initialised fixed list of strings"""
193 | return list(map(str, self.olist))
194 |
195 |
196 | class AllCompleter(Completer):
197 | """Completes by listing all possible files in current directory."""
198 |
199 | CALL_ARGS_OPTIONAL = ["pwd"]
200 |
201 | def _call(self, **kwargs):
202 | return os.listdir(kwargs.get("pwd", "."))
203 |
204 |
205 | class FileCompleter(Completer):
206 | """Completes by listing all possible files in current directory.
207 | If endings are specified, then limit the files to those."""
208 |
209 | CALL_ARGS_OPTIONAL = ["prefix"]
210 |
211 | def __init__(self, endings=None):
212 | if isinstance(endings, str):
213 | endings = [endings]
214 | elif endings is None:
215 | endings = []
216 | self.endings = tuple(map(str, endings))
217 |
218 | def _call(self, **kwargs):
219 | # TODO : what does prefix do in bash?
220 | prefix = kwargs.get("prefix", "")
221 |
222 | if SHELL == BASH:
223 | res = ["_filedir"]
224 | if self.endings:
225 | res.append(f"'@({'|'.join(self.endings)})'")
226 | return " ".join(res)
227 | else:
228 | res = []
229 | for path in glob.glob(prefix + "*"):
230 | res.append(path)
231 | if os.path.isdir(path):
232 | # add trailing slashes to directories
233 | res[-1] += os.path.sep
234 |
235 | if self.endings:
236 | res = [path for path in res if os.path.isdir(path) or path.endswith(self.endings)]
237 |
238 | if len(res) == 1 and os.path.isdir(res[0]):
239 | # return two options so that it completes the / but doesn't add a space
240 | return [res[0] + "a", res[0] + "b"]
241 | else:
242 | return res
243 |
244 |
245 | class DirCompleter(Completer):
246 | """Completes by listing subdirectories only."""
247 |
248 | CALL_ARGS_OPTIONAL = ["prefix"]
249 |
250 | def _call(self, **kwargs):
251 | # TODO : what does prefix do in bash?
252 | prefix = kwargs.get("prefix", "")
253 |
254 | if SHELL == BASH:
255 | return "_filedir -d"
256 | else:
257 | res = [path + "/" for path in glob.glob(prefix + "*") if os.path.isdir(path)]
258 |
259 | if len(res) == 1:
260 | # return two options so that it completes the / but doesn't add a space
261 | return [res[0] + "a", res[0] + "b"]
262 | else:
263 | return res
264 |
265 |
266 | class KnownHostsCompleter(Completer):
267 | """Completes a list of known hostnames"""
268 |
269 | def _call(self, **kwargs):
270 | if SHELL == BASH:
271 | return "_known_hosts"
272 | else:
273 | # TODO needs implementation, no autocompletion for now
274 | return []
275 |
276 |
277 | class RegexCompleter(Completer):
278 | """Completes by filtering all possible files with the given list of regexps."""
279 |
280 | CALL_ARGS_OPTIONAL = ["prefix", "pwd"]
281 |
282 | def __init__(self, regexlist, always_dirs=True):
283 | self.always_dirs = always_dirs
284 |
285 | if isinstance(regexlist, str):
286 | regexlist = [regexlist]
287 | self.regexlist = []
288 | for regex in regexlist:
289 | if isinstance(regex, str):
290 | regex = re.compile(regex)
291 | self.regexlist.append(regex)
292 |
293 | def _call(self, **kwargs):
294 | dn = os.path.dirname(kwargs.get("prefix", ""))
295 | if dn:
296 | pwd = dn
297 | else:
298 | pwd = kwargs.get("pwd", ".")
299 |
300 | ofiles = []
301 | for fn in os.listdir(pwd):
302 | for r in self.regexlist:
303 | if r.match(fn):
304 | if dn:
305 | fn = os.path.join(dn, fn)
306 | ofiles.append(fn)
307 | break
308 |
309 | if self.always_dirs and os.path.isdir(fn):
310 | ofiles.append(fn + os.path.sep)
311 |
312 | return ofiles
313 |
314 |
315 | class CompleterOption(OPTION_CLASS):
316 | """optparse Option class with completer attribute"""
317 |
318 | def __init__(self, *args, **kwargs):
319 | completer = kwargs.pop("completer", None)
320 | OPTION_CLASS.__init__(self, *args, **kwargs)
321 | if completer is not None:
322 | self.completer = completer
323 |
324 |
325 | def extract_word(line, point):
326 | """Return a prefix and suffix of the enclosing word. The character under
327 | the cursor is the first character of the suffix."""
328 |
329 | if SHELL == BASH and "IFS" in os.environ:
330 | ifs = [r.group(0) for r in re.finditer(r".", os.environ["IFS"])]
331 | wsre = re.compile("|".join(ifs))
332 | else:
333 | wsre = re.compile(r"\s")
334 |
335 | if point < 0 or point > len(line):
336 | return "", ""
337 |
338 | preii = point - 1
339 | while preii >= 0:
340 | if wsre.match(line[preii]):
341 | break
342 | preii -= 1
343 | preii += 1
344 |
345 | sufii = point
346 | while sufii < len(line):
347 | if wsre.match(line[sufii]):
348 | break
349 | sufii += 1
350 |
351 | return line[preii:point], line[point:sufii]
352 |
353 |
354 | def error_override(self, msg):
355 | """Hack to keep OptionParser from writing to sys.stderr when
356 | calling self.exit from self.error"""
357 | self.exit(2, msg=msg)
358 |
359 |
360 | def guess_first_nonoption(gparser, subcmds_map):
361 | """Given a global options parser, try to guess the first non-option without
362 | generating an exception. This is used for scripts that implement a
363 | subcommand syntax, so that we can generate the appropriate completions for
364 | the subcommand."""
365 |
366 | gparser = copy.deepcopy(gparser)
367 |
368 | def print_usage_nousage(self, *args, **kwargs): # pylint: disable=unused-argument
369 | pass
370 |
371 | gparser.print_usage = print_usage_nousage
372 |
373 | prev_interspersed = gparser.allow_interspersed_args # save state to restore
374 | gparser.disable_interspersed_args()
375 |
376 | # interpret cwords like a shell would interpret it
377 | cwords = shlex.split(os.environ.get("COMP_WORDS", "").strip("() "))
378 |
379 | # save original error_func so we can put it back after the hack
380 | error_func = gparser.error
381 | try:
382 | try:
383 | instancemethod = type(OPTIONPARSER_CLASS.error)
384 | # hack to keep OptionParser from writing to sys.stderr
385 | gparser.error = instancemethod(error_override, gparser, OPTIONPARSER_CLASS)
386 | _, args = gparser.parse_args(cwords[1:])
387 | except SystemExit:
388 | return None
389 | finally:
390 | # undo the hack and restore original OptionParser error function
391 | gparser.error = instancemethod(error_func, gparser, OPTIONPARSER_CLASS)
392 |
393 | value = None
394 | if args:
395 | subcmdname = args[0]
396 | try:
397 | value = subcmds_map[subcmdname]
398 | except KeyError:
399 | pass
400 |
401 | gparser.allow_interspersed_args = prev_interspersed # restore state
402 |
403 | return value # can be None, indicates no command chosen.
404 |
405 |
406 | def autocomplete(parser, arg_completer=None, opt_completer=None, subcmd_completer=None, subcommands=None):
407 | """Automatically detect if we are requested completing and if so generate
408 | completion automatically from given parser.
409 |
410 | 'parser' is the options parser to use.
411 |
412 | 'arg_completer' is a callable object that gets invoked to produce a list of
413 | completions for arguments completion (oftentimes files).
414 |
415 | 'opt_completer' is the default completer to the options that require a
416 | value.
417 |
418 | 'subcmd_completer' is the default completer for the subcommand
419 | arguments.
420 |
421 | If 'subcommands' is specified, the script expects it to be a map of
422 | command-name to an object of any kind. We are assuming that this object is
423 | a map from command name to a pair of (options parser, completer) for the
424 | command. If the value is not such a tuple, the method
425 | 'autocomplete(completer)' is invoked on the resulting object.
426 |
427 | This will attempt to match the first non-option argument into a subcommand
428 | name and if so will use the local parser in the corresponding map entry's
429 | value. This is used to implement completion for subcommand syntax and will
430 | not be needed in most cases.
431 | """
432 |
433 | # If we are not requested for complete, simply return silently, let the code
434 | # caller complete. This is the normal path of execution.
435 | if OPTCOMPLETE_ENVIRONMENT not in os.environ:
436 | return
437 | # After this point we should never return, only sys.exit(1)
438 |
439 | # Set default completers.
440 | if arg_completer is None:
441 | arg_completer = NoneCompleter()
442 | if opt_completer is None:
443 | opt_completer = FileCompleter()
444 | if subcmd_completer is None:
445 | # subcmd_completer = arg_completer
446 | subcmd_completer = FileCompleter()
447 |
448 | # By default, completion will be arguments completion, unless we find out
449 | # later we're trying to complete for an option.
450 | completer = arg_completer
451 |
452 | #
453 | # Completing...
454 | #
455 |
456 | # Fetching inputs... not sure if we're going to use these.
457 |
458 | # zsh's bashcompinit does not pass COMP_WORDS, replace with
459 | # COMP_LINE for now...
460 | if "COMP_WORDS" not in os.environ:
461 | os.environ["COMP_WORDS"] = os.environ["COMP_LINE"]
462 |
463 | cwords = shlex.split(os.environ.get("COMP_WORDS", "").strip("() "))
464 | cline = os.environ.get("COMP_LINE", "")
465 | cpoint = int(os.environ.get("COMP_POINT", 0))
466 | cword = int(os.environ.get("COMP_CWORD", 0))
467 |
468 | # Extract word enclosed word.
469 | prefix, suffix = extract_word(cline, cpoint)
470 |
471 | # If requested, try subcommand syntax to find an options parser for that
472 | # subcommand.
473 | if subcommands:
474 | assert isinstance(subcommands, dict)
475 | value = guess_first_nonoption(parser, subcommands)
476 | if value:
477 | if isinstance(value, (list, tuple)):
478 | parser = value[0]
479 | if len(value) > 1 and value[1]:
480 | # override completer for command if it is present.
481 | completer = value[1]
482 | else:
483 | completer = subcmd_completer
484 | autocomplete(parser, completer)
485 | elif hasattr(value, "autocomplete"):
486 | # Call completion method on object. This should call
487 | # autocomplete() recursively with appropriate arguments.
488 | value.autocomplete(subcmd_completer)
489 | else:
490 | # no completions for that command object
491 | pass
492 | sys.exit(1)
493 | else: # suggest subcommands
494 | completer = ListCompleter(subcommands.keys())
495 |
496 | # Look at previous word, if it is an option and it requires an argument,
497 | # check for a local completer. If there is no completer, what follows
498 | # directly cannot be another option, so mark to not add those to
499 | # completions.
500 | optarg = False
501 | try:
502 | # Look for previous word, which will be containing word if the option
503 | # has an equals sign in it.
504 | prev = None
505 | if cword < len(cwords):
506 | mo = re.search("(--.*?)=(.*)", cwords[cword])
507 | if mo:
508 | prev, prefix = mo.groups()
509 | if not prev:
510 | prev = cwords[cword - 1]
511 |
512 | if prev and prev.startswith("-"):
513 | option = parser.get_option(prev)
514 | if option:
515 | if option.nargs > 0:
516 | optarg = True
517 | if hasattr(option, "completer"):
518 | completer = option.completer
519 | elif option.choices:
520 | completer = ListCompleter(option.choices)
521 | elif option.type in ("string",):
522 | completer = opt_completer
523 | else:
524 | completer = NoneCompleter()
525 | # Warn user at least, it could help him figure out the problem.
526 | elif hasattr(option, "completer"):
527 | msg = f"Error: optparse option with a completer does not take arguments: {option}"
528 | raise SystemExit(msg)
529 | except KeyError:
530 | pass
531 |
532 | completions = []
533 |
534 | # Options completion.
535 | if not optarg and (not prefix or prefix.startswith("-")):
536 | completions += parser._short_opt.keys()
537 | completions += parser._long_opt.keys()
538 | # Note: this will get filtered properly below.
539 |
540 | completer_kwargs = {
541 | "pwd": os.getcwd(),
542 | "cline": cline,
543 | "cpoint": cpoint,
544 | "prefix": prefix,
545 | "suffix": suffix,
546 | }
547 | # File completion.
548 | if completer and (not prefix or not prefix.startswith("-")):
549 | # Call appropriate completer depending on type.
550 | if isinstance(completer, (list, tuple)) or isinstance(completer, str):
551 | completer = FileCompleter(completer)
552 | elif not isinstance(completer, (types.FunctionType, types.LambdaType, types.ClassType, types.ObjectType)):
553 | # TODO: what to do here?
554 | pass
555 |
556 | completions = completer(**completer_kwargs)
557 |
558 | if isinstance(completions, str):
559 | # is a bash command, just run it
560 | if SHELL in (BASH,): # TODO: zsh
561 | print(completions)
562 | else:
563 | raise Exception(f"Commands are unsupported by this shell {SHELL}")
564 | else:
565 | # Filter using prefix.
566 | if prefix:
567 | completions = sorted(filter(lambda x: x.startswith(prefix), completions))
568 | completions = " ".join(map(str, completions))
569 |
570 | # Save results
571 | if SHELL == "bash":
572 | print("COMPREPLY=(" + completions + ")")
573 | else:
574 | print(completions)
575 |
576 | # Print debug output (if needed). You can keep a shell with 'tail -f' to
577 | # the log file to monitor what is happening.
578 | if debugfn:
579 | txt = "\n".join([
580 | "---------------------------------------------------------",
581 | f"CWORDS {cwords}",
582 | f"CLINE {cline}",
583 | f"CPOINT {cpoint}",
584 | f"CWORD {cword}",
585 | "",
586 | "Short options",
587 | pformat(parser._short_opt),
588 | "",
589 | "Long options",
590 | pformat(parser._long_opt),
591 | f"Prefix {prefix}",
592 | f"Suffix {suffix}",
593 | f"completer_kwargs{str(completer_kwargs)}",
594 | #'completer_completions %s' % completer_completions,
595 | f"completions {completions}",
596 | ])
597 | if isinstance(debugfn, logging.Logger):
598 | debugfn.debug(txt)
599 | else:
600 | with open(debugfn, "a") as fih:
601 | fih.write(txt)
602 |
603 | # Exit with error code (we do not let the caller continue on purpose, this
604 | # is a run for completions only.)
605 | sys.exit(1)
606 |
607 |
608 | class CmdComplete:
609 | """Simple default base class implementation for a subcommand that supports
610 | command completion. This class is assuming that there might be a method
611 | addopts(self, parser) to declare options for this subcommand, and an
612 | optional completer data member to contain command-specific completion. Of
613 | course, you don't really have to use this, but if you do it is convenient to
614 | have it here."""
615 |
616 | def autocomplete(self, completer=None):
617 | parser = OPTIONPARSER_CLASS(self.__doc__.strip())
618 | if hasattr(self, "addopts"):
619 | fnc = getattr(self, "addopts")
620 | fnc(parser)
621 |
622 | if hasattr(self, "completer"):
623 | completer = getattr(self, "completer")
624 |
625 | return autocomplete(parser, completer)
626 |
627 |
628 | def gen_cmdline(cmd_list, partial, shebang=True):
629 | """Create the commandline to generate simulated tabcompletion output
630 | @param cmd_list: command to execute as list of strings
631 | @param partial: the string to autocomplete (typically, partial is an element of the cmd_list)
632 | @param shebang: script has python shebang (if not, add sys.executable)
633 | """
634 | cmdline = " ".join([shell_quote(cmd) for cmd in cmd_list])
635 |
636 | env = []
637 | env.append(f"{OPTCOMPLETE_ENVIRONMENT}=1")
638 | env.append(f'COMP_LINE="{cmdline}"')
639 | env.append(f'COMP_WORDS="({cmdline})"')
640 | env.append(f"COMP_POINT={len(cmdline)}")
641 | env.append(f"COMP_CWORD={cmd_list.index(partial)}")
642 |
643 | if not shebang:
644 | env.append(shell_quote(sys.executable))
645 |
646 | # add script
647 | env.append(f'"{cmd_list[0]}"')
648 |
649 | return " ".join(env)
650 |
--------------------------------------------------------------------------------