diff --git a/.github/conda-env/test-env.yml b/.github/conda-env/test-env.yml index cc91a1ade..a4944f768 100644 --- a/.github/conda-env/test-env.yml +++ b/.github/conda-env/test-env.yml @@ -6,6 +6,7 @@ dependencies: - pytest - pytest-cov - pytest-timeout + - pytest-xvfb - numpy - matplotlib - - scipy \ No newline at end of file + - scipy diff --git a/.github/workflows/control-slycot-src.yml b/.github/workflows/control-slycot-src.yml index ffbeca3f9..2ce2a11dd 100644 --- a/.github/workflows/control-slycot-src.yml +++ b/.github/workflows/control-slycot-src.yml @@ -13,19 +13,8 @@ jobs: path: python-control - name: Set up Python uses: actions/setup-python@v2 - - name: Install Python dependencies - run: | - # Set up conda - echo $CONDA/bin >> $GITHUB_PATH - - # Set up (virtual) X11 - sudo apt install -y xvfb - - # Install test tools - conda install pip pytest pytest-timeout - - # Install python-control dependencies - conda install numpy matplotlib scipy + - name: Install Python dependencies and test tools + run: pip install -v -e './python-control[test]' - name: Checkout Slycot uses: actions/checkout@v3 @@ -43,11 +32,10 @@ jobs: # Install compilers, libraries, and development environment sudo apt-get -y install gfortran cmake --fix-missing sudo apt-get -y install libblas-dev liblapack-dev - conda install -c conda-forge scikit-build setuptools-scm # Compile and install slycot - pip install -v --no-build-isolation --no-deps . + pip install -v . - name: Test with pytest working-directory: python-control - run: xvfb-run --auto-servernum pytest control/tests + run: pytest -v control/tests diff --git a/.github/workflows/install_examples.yml b/.github/workflows/install_examples.yml index b36ff3e7f..84cd706f5 100644 --- a/.github/workflows/install_examples.yml +++ b/.github/workflows/install_examples.yml @@ -1,4 +1,4 @@ -name: setup.py, examples +name: Setup, Examples, Notebooks on: [push, pull_request] @@ -7,26 +7,23 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - - name: Install Python dependencies + - uses: actions/checkout@v3 + - name: Install Python dependencies from conda-forge run: | - # Set up conda + # Set up conda using the preinstalled GHA Miniconda environment echo $CONDA/bin >> $GITHUB_PATH + conda config --add channels conda-forge + conda config --set channel_priority strict - # Set up (virtual) X11 - sudo apt install -y xvfb + # Install build tools + conda install pip setuptools setuptools-scm - # Install test tools - conda install pip pytest + # Install python-control dependencies and extras + conda install numpy matplotlib scipy + conda install slycot pmw jupyter - # Install python-control dependencies - conda install numpy matplotlib scipy jupyter - conda install -c conda-forge slycot pmw - - - name: Install with setup.py - run: python setup.py install + - name: Install from source + run: pip install . - name: Run examples run: | diff --git a/.github/workflows/python-package-conda.yml b/.github/workflows/python-package-conda.yml index 4e287b45a..ae889fd05 100644 --- a/.github/workflows/python-package-conda.yml +++ b/.github/workflows/python-package-conda.yml @@ -8,30 +8,32 @@ jobs: Py${{ matrix.python-version }}; ${{ matrix.slycot || 'no' }} Slycot; ${{ matrix.pandas || 'no' }} Pandas; - ${{ matrix.cvxopt || 'no' }} CVXOPT; + ${{ matrix.cvxopt || 'no' }} CVXOPT ${{ matrix.array-and-matrix == 1 && '; array and matrix' || '' }} + ${{ matrix.mplbackend && format('; {0}', matrix.mplbackend) }} runs-on: ubuntu-latest strategy: max-parallel: 5 + fail-fast: false matrix: - python-version: [3.7, 3.9] + python-version: ['3.7', '3.10'] slycot: ["", "conda"] pandas: [""] cvxopt: ["", "conda"] + mplbackend: [""] array-and-matrix: [0] include: - - python-version: 3.9 + - python-version: '3.10' slycot: conda pandas: conda + cvxopt: conda + mplbackend: QtAgg array-and-matrix: 1 steps: - uses: actions/checkout@v3 - - name: Set up (virtual) X11 - run: sudo apt install -y xvfb - - name: Setup Conda uses: conda-incubator/setup-miniconda@v2 with: @@ -55,15 +57,15 @@ jobs: mamba install pandas fi if [[ '${{matrix.cvxopt}}' == 'conda' ]]; then - mamba install cvxopt + mamba install cvxopt fi - name: Test with pytest shell: bash -l {0} env: PYTHON_CONTROL_ARRAY_AND_MATRIX: ${{ matrix.array-and-matrix }} - run: | - xvfb-run --auto-servernum pytest --cov=control --cov-config=.coveragerc control/tests + MPLBACKEND: ${{ matrix.mplbackend }} + run: pytest -v --cov=control --cov-config=.coveragerc control/tests - name: Coveralls parallel # https://github.com/coverallsapp/github-action diff --git a/README.rst b/README.rst index f1feda7c5..7e2058293 100644 --- a/README.rst +++ b/README.rst @@ -97,17 +97,14 @@ To install using pip:: If you install Slycot using pip you'll need a development environment (e.g., Python development files, C and Fortran compilers). -Distutils ---------- +Installing from source +---------------------- -To install in your home directory, use:: +To install from source, get the source code of the desired branch or release +from the github repository or archive, unpack, and run from within the +toplevel `python-control` directory:: - python setup.py install --user - -To install for all users (on Linux or Mac OS):: - - python setup.py build - sudo python setup.py install + pip install . Development diff --git a/control/tests/config_test.py b/control/tests/config_test.py index 295c68bdd..c36f67280 100644 --- a/control/tests/config_test.py +++ b/control/tests/config_test.py @@ -8,7 +8,6 @@ from math import pi, log10 import matplotlib.pyplot as plt -from matplotlib.testing.decorators import cleanup as mplcleanup import numpy as np import pytest @@ -18,7 +17,6 @@ @pytest.mark.usefixtures("editsdefaults") # makes sure to reset the defaults # to the test configuration class TestConfig: - # Create a simple second order system to use for testing sys = ct.tf([10], [1, 2, 1]) @@ -28,8 +26,7 @@ def test_set_defaults(self): assert ct.config.defaults['freqplot.deg'] == 2 assert ct.config.defaults['freqplot.Hz'] is None - @mplcleanup - def test_get_param(self): + def test_get_param(self, mplcleanup): assert ct.config._get_param('freqplot', 'dB')\ == ct.config.defaults['freqplot.dB'] assert ct.config._get_param('freqplot', 'dB', 1) == 1 @@ -92,8 +89,7 @@ def test_default_deprecation(self): assert ct.config.defaults['bode.Hz'] \ == ct.config.defaults['freqplot.Hz'] - @mplcleanup - def test_fbs_bode(self): + def test_fbs_bode(self, mplcleanup): ct.use_fbs_defaults() # Generate a Bode plot @@ -137,8 +133,7 @@ def test_fbs_bode(self): phase_x, phase_y = (((plt.gcf().axes[1]).get_lines())[0]).get_data() np.testing.assert_almost_equal(phase_y[-1], -pi, decimal=2) - @mplcleanup - def test_matlab_bode(self): + def test_matlab_bode(self, mplcleanup): ct.use_matlab_defaults() # Generate a Bode plot @@ -182,8 +177,7 @@ def test_matlab_bode(self): phase_x, phase_y = (((plt.gcf().axes[1]).get_lines())[0]).get_data() np.testing.assert_almost_equal(phase_y[-1], -pi, decimal=2) - @mplcleanup - def test_custom_bode_default(self): + def test_custom_bode_default(self, mplcleanup): ct.config.defaults['freqplot.dB'] = True ct.config.defaults['freqplot.deg'] = True ct.config.defaults['freqplot.Hz'] = True @@ -204,8 +198,7 @@ def test_custom_bode_default(self): np.testing.assert_almost_equal(mag_y[0], 20*log10(10), decimal=3) np.testing.assert_almost_equal(phase_y[-1], -pi, decimal=2) - @mplcleanup - def test_bode_number_of_samples(self): + def test_bode_number_of_samples(self, mplcleanup): # Set the number of samples (default is 50, from np.logspace) mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, omega_num=87) assert len(mag_ret) == 87 @@ -219,8 +212,7 @@ def test_bode_number_of_samples(self): mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, omega_num=87) assert len(mag_ret) == 87 - @mplcleanup - def test_bode_feature_periphery_decade(self): + def test_bode_feature_periphery_decade(self, mplcleanup): # Generate a sample Bode plot to figure out the range it uses ct.reset_defaults() # Make sure starting state is correct mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, Hz=False) diff --git a/control/tests/conftest.py b/control/tests/conftest.py index 1201b8746..3f798f26c 100644 --- a/control/tests/conftest.py +++ b/control/tests/conftest.py @@ -1,13 +1,11 @@ """conftest.py - pytest local plugins and fixtures""" import os -import sys from contextlib import contextmanager import matplotlib as mpl import numpy as np import pytest -import scipy as sp import control @@ -45,7 +43,7 @@ def control_defaults(): params=[pytest.param("arrayout", marks=matrixerrorfilter), pytest.param("matrixout", marks=matrixfilter)]) def matarrayout(request): - """Switch the config to use np.ndarray and np.matrix as returns""" + """Switch the config to use np.ndarray and np.matrix as returns.""" restore = control.config.defaults['statesp.use_numpy_matrix'] control.use_numpy_matrix(request.param == "matrixout", warn=False) yield @@ -53,7 +51,7 @@ def matarrayout(request): def ismatarrayout(obj): - """Test if the returned object has the correct type as configured + """Test if the returned object has the correct type as configured. note that isinstance(np.matrix(obj), np.ndarray) is True """ @@ -63,7 +61,7 @@ def ismatarrayout(obj): def asmatarrayout(obj): - """Return a object according to the configured default""" + """Return a object according to the configured default.""" use_matrix = control.config.defaults['statesp.use_numpy_matrix'] matarray = np.asmatrix if use_matrix else np.asarray return matarray(obj) @@ -71,7 +69,7 @@ def asmatarrayout(obj): @contextmanager def check_deprecated_matrix(): - """Check that a call produces a deprecation warning because of np.matrix""" + """Check that a call produces a deprecation warning because of np.matrix.""" use_matrix = control.config.defaults['statesp.use_numpy_matrix'] if use_matrix: with pytest.deprecated_call(): @@ -94,13 +92,13 @@ def check_deprecated_matrix(): False)] if usebydefault or TEST_MATRIX_AND_ARRAY]) def matarrayin(request): - """Use array and matrix to construct input data in tests""" + """Use array and matrix to construct input data in tests.""" return request.param @pytest.fixture(scope="function") def editsdefaults(): - """Make sure any changes to the defaults only last during a test""" + """Make sure any changes to the defaults only last during a test.""" restore = control.config.defaults.copy() yield control.config.defaults = restore.copy() diff --git a/control/tests/rlocus_test.py b/control/tests/rlocus_test.py index a0ecebb15..4fbe70c4f 100644 --- a/control/tests/rlocus_test.py +++ b/control/tests/rlocus_test.py @@ -85,6 +85,8 @@ def test_root_locus_neg_false_gain_nonproper(self): # TODO: cover and validate negative false_gain branch in _default_gains() + @pytest.mark.skipif(plt.get_current_fig_manager().toolbar is None, + reason="Requires the zoom toolbar") def test_root_locus_zoom(self): """Check the zooming functionality of the Root locus plot""" system = TransferFunction([1000], [1, 25, 100, 0]) diff --git a/control/tests/sisotool_test.py b/control/tests/sisotool_test.py index d5e9dd013..a1f468eea 100644 --- a/control/tests/sisotool_test.py +++ b/control/tests/sisotool_test.py @@ -46,6 +46,8 @@ def sys221(self): D221 = [[1., -1.]] return StateSpace(A222, B222, C221, D221) + @pytest.mark.skipif(plt.get_current_fig_manager().toolbar is None, + reason="Requires the zoom toolbar") def test_sisotool(self, tsys): sisotool(tsys, Hz=False) fig = plt.gcf() @@ -114,6 +116,8 @@ def test_sisotool(self, tsys): assert_array_almost_equal( ax_step.lines[0].get_data()[1][:10], step_response_moved, 4) + @pytest.mark.skipif(plt.get_current_fig_manager().toolbar is None, + reason="Requires the zoom toolbar") @pytest.mark.parametrize('tsys', [0, True], indirect=True, ids=['ctime', 'dtime']) def test_sisotool_tvect(self, tsys): diff --git a/make_version.py b/make_version.py deleted file mode 100644 index 356f4d747..000000000 --- a/make_version.py +++ /dev/null @@ -1,58 +0,0 @@ -# make_version.py - generate version information -# -# Author: Clancy Rowley -# Date: 2 Apr 2015 -# Modified: Richard M. Murray, 28 Dec 2017 -# -# This script is used to create the version information for the python- -# control package. The version information is now generated directly from -# tags in the git repository. Now, *before* running setup.py, one runs -# -# python make_version.py -# -# and this generates a file with the version information. This is copied -# from binstar (https://github.com/Binstar/binstar) and seems to work well. -# -# The original version of this script also created version information for -# conda, but this stopped working when conda v3 was released. Instead, we -# now use jinja templates in conda-recipe to create the conda information. -# The current version information is used in setup.py, control/__init__.py, -# and doc/conf.py (for sphinx). - -from subprocess import check_output -import os - -def main(): - cmd = 'git describe --always --long' - # describe --long usually outputs "tag-numberofcommits-commitname" - output = check_output(cmd.split()).decode('utf-8').strip().rsplit('-',2) - if len(output) == 3: - version, build, commit = output - else: - # If the clone is shallow, describe's output won't have tag and - # number of commits. This is a particular issue on Travis-CI, - # which by default clones with a depth of 50. - # This behaviour isn't well documented in git-describe docs, - # but see, e.g., https://stackoverflow.com/a/36389573/1008142 - # and https://github.com/travis-ci/travis-ci/issues/3412 - version = 'unknown' - build = 'unknown' - # we don't ever expect just one dash from describe --long, but - # just in case: - commit = '-'.join(output) - - print("Version: %s" % version) - print("Build: %s" % build) - print("Commit: %s\n" % commit) - - filename = "control/_version.py" - print("Writing %s" % filename) - with open(filename, 'w') as fd: - if build == '0': - fd.write('__version__ = "%s"\n' % (version)) - else: - fd.write('__version__ = "%s.post%s"\n' % (version, build)) - fd.write('__commit__ = "%s"\n' % (commit)) - -if __name__ == '__main__': - main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..89690ac8d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,58 @@ +[build-system] +requires = [ + "setuptools", + "setuptools-scm", + "wheel" +] +build-backend = "setuptools.build_meta" + +[project] +name = "control" +description = "Python Control Systems Library" +authors = [{name = "Python Control Developers", email = "python-control-developers@lists.sourceforge.net"}] +license = {text = "BSD-3-Clause"} +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Software Development", + "Topic :: Scientific/Engineering", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Operating System :: Unix", + "Operating System :: MacOS", +] +requires-python = ">=3.7" +dependencies = [ + "numpy", + "scipy>=1.3", + "matplotlib", +] +dynamic = ["version"] + +[tool.setuptools] +packages = ["control"] + +[project.optional-dependencies] +test = ["pytest", "pytest-timeout"] +slycot = [ "slycot>=0.4.0" ] +cvxopt = [ "cvxopt>=1.2.0" ] + +[project.urls] +homepage = "https//python-control.org" +source = "https://github.com/python-control/python-control" + +[tool.setuptools_scm] +write_to = "control/_version.py" + +[tool.pytest.ini_options] +addopts = "-ra" +filterwarnings = [ + "error:.*matrix subclass:PendingDeprecationWarning", +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 5b1ce28a7..000000000 --- a/setup.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[bdist_wheel] -universal=1 - -[tool:pytest] -addopts = -ra -filterwarnings = - error:.*matrix subclass:PendingDeprecationWarning diff --git a/setup.py b/setup.py deleted file mode 100644 index 2021d5eb9..000000000 --- a/setup.py +++ /dev/null @@ -1,52 +0,0 @@ -from setuptools import setup, find_packages - -ver = {} -try: - with open('control/_version.py') as fd: - exec(fd.read(), ver) - version = ver.get('__version__', 'dev') -except IOError: - version = 'dev' - -with open('README.rst') as fp: - long_description = fp.read() - -CLASSIFIERS = """ -Development Status :: 3 - Alpha -Intended Audience :: Science/Research -Intended Audience :: Developers -License :: OSI Approved :: BSD License -Programming Language :: Python :: 3 -Programming Language :: Python :: 3.7 -Programming Language :: Python :: 3.8 -Programming Language :: Python :: 3.9 -Topic :: Software Development -Topic :: Scientific/Engineering -Operating System :: Microsoft :: Windows -Operating System :: POSIX -Operating System :: Unix -Operating System :: MacOS -""" - -setup( - name='control', - version=version, - author='Python Control Developers', - author_email='python-control-developers@lists.sourceforge.net', - url='http://python-control.org', - project_urls={ - 'Source': 'https://github.com/python-control/python-control', - }, - description='Python Control Systems Library', - long_description=long_description, - packages=find_packages(exclude=['benchmarks']), - classifiers=[f for f in CLASSIFIERS.split('\n') if f], - install_requires=['numpy', - 'scipy>=1.3', - 'matplotlib'], - extras_require={ - 'test': ['pytest', 'pytest-timeout'], - 'slycot': [ 'slycot>=0.4.0' ], - 'cvxopt': [ 'cvxopt>=1.2.0' ] - } -)
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: