Skip to content

Commit 2e0f0d0

Browse files
committed
Add standard Numpy testing
setup.py and runtests.py taken from SciKit example at http://scikits.appspot.com/example
1 parent 93b1670 commit 2e0f0d0

6 files changed

Lines changed: 337 additions & 14 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
*.pyc
22
build/
33
test/
4+
control/version.py
5+
build.log
6+
*.egg-info/

control/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,9 @@
9797
# bode and nyquist come directly from freqplot.py
9898
from control.matlab import step, impulse, initial, lsim
9999
from control.matlab import ssdata, tfdata
100+
101+
# The following is to use Numpy's testing framework
102+
# Tests go under directory tests/, benchmarks under directory benchmarks/
103+
from numpy.testing import Tester
104+
test = Tester().test
105+
bench = Tester().bench

control/setup.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
def configuration(parent_package='', top_path=None):
2+
from numpy.distutils.misc_util import Configuration
3+
config = Configuration('control', parent_package, top_path)
4+
config.add_subpackage('tests')
5+
return config

control/tests/__init__.py

Whitespace-only changes.

runtests.py

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
#!/usr/bin/env python
2+
"""
3+
runtests.py [OPTIONS] [-- ARGS]
4+
5+
Run tests, building the project first.
6+
7+
Examples::
8+
9+
$ python runtests.py
10+
$ python runtests.py -t {SAMPLE_TEST}
11+
$ python runtests.py --ipython
12+
13+
"""
14+
15+
#
16+
# This is a generic test runner script for projects using Numpy's test
17+
# framework. Change the following values to adapt to your project:
18+
#
19+
20+
PROJECT_MODULE = "control"
21+
PROJECT_ROOT_FILES = ['setup.py']
22+
SAMPLE_TEST = "control/tests/margin_test.py"
23+
24+
# ---------------------------------------------------------------------
25+
26+
__doc__ = __doc__.format(**globals())
27+
28+
import sys
29+
import os
30+
31+
# In case we are run from the source directory, we don't want to import the
32+
# project from there:
33+
sys.path.pop(0)
34+
35+
import shutil
36+
import subprocess
37+
from argparse import ArgumentParser, REMAINDER
38+
39+
def main(argv):
40+
parser = ArgumentParser(usage=__doc__.lstrip())
41+
parser.add_argument("--verbose", "-v", action="count", default=1,
42+
help="more verbosity")
43+
parser.add_argument("--no-build", "-n", action="store_true", default=False,
44+
help="do not build the project (use system installed version)")
45+
parser.add_argument("--build-only", "-b", action="store_true", default=False,
46+
help="just build, do not run any tests")
47+
parser.add_argument("--doctests", action="store_true", default=False,
48+
help="Run doctests in module")
49+
parser.add_argument("--coverage", action="store_true", default=False,
50+
help=("report coverage of project code. HTML output goes "
51+
"under build/coverage"))
52+
parser.add_argument("--mode", "-m", default="fast",
53+
help="'fast', 'full', or something that could be "
54+
"passed to nosetests -A [default: fast]")
55+
parser.add_argument("--submodule", "-s", default=None,
56+
help="Submodule whose tests to run (cluster, constants, ...)")
57+
parser.add_argument("--pythonpath", "-p", default=None,
58+
help="Paths to prepend to PYTHONPATH")
59+
parser.add_argument("--tests", "-t", action='append',
60+
help="Specify tests to run")
61+
parser.add_argument("--python", action="store_true",
62+
help="Start a Python shell with PYTHONPATH set")
63+
parser.add_argument("--ipython", "-i", action="store_true",
64+
help="Start IPython shell with PYTHONPATH set")
65+
parser.add_argument("--shell", action="store_true",
66+
help="Start Unix shell with PYTHONPATH set")
67+
parser.add_argument("--debug", "-g", action="store_true",
68+
help="Debug build")
69+
parser.add_argument("args", metavar="ARGS", default=[], nargs=REMAINDER,
70+
help="Arguments to pass to Nose")
71+
args = parser.parse_args(argv)
72+
73+
if args.pythonpath:
74+
for p in reversed(args.pythonpath.split(os.pathsep)):
75+
sys.path.insert(0, p)
76+
77+
if not args.no_build:
78+
site_dir = build_project(args)
79+
sys.path.insert(0, site_dir)
80+
os.environ['PYTHONPATH'] = site_dir
81+
82+
if args.python:
83+
import code
84+
code.interact()
85+
sys.exit(0)
86+
87+
if args.ipython:
88+
import IPython
89+
IPython.embed()
90+
sys.exit(0)
91+
92+
if args.shell:
93+
shell = os.environ.get('SHELL', 'sh')
94+
print("Spawning a Unix shell...")
95+
os.execv(shell, [shell])
96+
sys.exit(1)
97+
98+
extra_argv = args.args
99+
100+
if args.coverage:
101+
dst_dir = os.path.join('build', 'coverage')
102+
fn = os.path.join(dst_dir, 'coverage_html.js')
103+
if os.path.isdir(dst_dir) and os.path.isfile(fn):
104+
shutil.rmtree(dst_dir)
105+
extra_argv += ['--cover-html',
106+
'--cover-html-dir='+dst_dir]
107+
108+
if args.build_only:
109+
sys.exit(0)
110+
elif args.submodule:
111+
modname = PROJECT_MODULE + '.' + args.submodule
112+
try:
113+
__import__(modname)
114+
test = sys.modules[modname].test
115+
except (ImportError, KeyError, AttributeError):
116+
print("Cannot run tests for %s" % modname)
117+
sys.exit(2)
118+
elif args.tests:
119+
def test(*a, **kw):
120+
extra_argv = kw.pop('extra_argv', ())
121+
extra_argv = extra_argv + args.tests[1:]
122+
kw['extra_argv'] = extra_argv
123+
from numpy.testing import Tester
124+
return Tester(args.tests[0]).test(*a, **kw)
125+
else:
126+
__import__(PROJECT_MODULE)
127+
test = sys.modules[PROJECT_MODULE].test
128+
129+
result = test(args.mode,
130+
verbose=args.verbose,
131+
extra_argv=args.args,
132+
doctests=args.doctests,
133+
coverage=args.coverage)
134+
135+
if result.wasSuccessful():
136+
sys.exit(0)
137+
else:
138+
sys.exit(1)
139+
140+
def build_project(args):
141+
"""
142+
Build a dev version of the project.
143+
144+
Returns
145+
-------
146+
site_dir
147+
site-packages directory where it was installed
148+
149+
"""
150+
151+
root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__)))
152+
root_ok = [os.path.exists(os.path.join(root_dir, fn))
153+
for fn in PROJECT_ROOT_FILES]
154+
if not all(root_ok):
155+
print("To build the project, run runtests.py in "
156+
"git checkout or unpacked source")
157+
sys.exit(1)
158+
159+
dst_dir = os.path.join(root_dir, 'build', 'testenv')
160+
161+
from distutils.sysconfig import get_python_lib
162+
site_dir = get_python_lib(prefix=dst_dir)
163+
164+
env = dict(os.environ)
165+
cmd = [sys.executable, 'setup.py']
166+
167+
# Always use ccache if available
168+
env['PATH'] = os.pathsep.join(['/usr/lib/ccache']
169+
+ env.get('PATH', '').split(os.pathsep))
170+
171+
if args.debug:
172+
# assume everyone uses gcc/gfortran
173+
env['OPT'] = '-O0 -ggdb'
174+
env['FOPT'] = '-O0 -ggdb'
175+
cmd += ["build", "--debug"]
176+
177+
cmd += ['install', '--prefix=' + dst_dir]
178+
179+
# Setup for setuptools
180+
cmd += ['--single-version-externally-managed',
181+
'--record=' + os.path.join(dst_dir, 'record.lst')]
182+
if not os.path.isdir(site_dir):
183+
os.makedirs(site_dir)
184+
env['PYTHONPATH'] = os.pathsep.join([site_dir]
185+
+ env.get('PATH', '').split(os.pathsep))
186+
187+
# Build it.
188+
print("Building, see build.log...")
189+
with open('build.log', 'w') as log:
190+
ret = subprocess.call(cmd, env=env, stdout=log, stderr=log,
191+
cwd=root_dir)
192+
193+
if ret == 0:
194+
print("Build OK")
195+
else:
196+
with open('build.log', 'r') as f:
197+
print(f.read())
198+
print("Build failed!")
199+
sys.exit(1)
200+
201+
return site_dir
202+
203+
if __name__ == "__main__":
204+
main(argv=sys.argv[1:])

setup.py

Lines changed: 119 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,121 @@
11
#!/usr/bin/env python
2+
descr = """Python Control Systems Library
23
3-
#from distutils.core import setup
4-
from setuptools import setup
5-
6-
setup(name = 'control',
7-
version = '0.6e',
8-
description = 'Python Control Systems Library',
9-
author = 'Richard Murray',
10-
author_email = 'murray@cds.caltech.edu',
11-
url = 'http://python-control.sourceforge.net',
12-
install_requires = ['scipy', 'matplotlib'],
13-
tests_require = ['scipy', 'matplotlib', 'nose'],
14-
packages = ['control'],
15-
test_suite='nose.collector'
16-
)
4+
The Python Control Systems Library, python-control,
5+
is a python module that implements basic operations
6+
for analysis and design of feedback control systems.
7+
8+
Features:
9+
Linear input/output systems in state space and frequency domain
10+
Block diagram algebra: serial, parallel and feedback interconnections
11+
Time response: initial, step, impulse
12+
Frequency response: Bode and Nyquist plots
13+
Control analysis: stability, reachability, observability, stability margins
14+
Control design: eigenvalue placement, linear quadratic regulator
15+
Estimator design: linear quadratic estimator (Kalman filter)
16+
17+
"""
18+
19+
DISTNAME = 'control'
20+
DESCRIPTION = 'Python control systems library'
21+
LONG_DESCRIPTION = descr
22+
AUTHOR = 'Richard Murray'
23+
AUTHOR_EMAIL = 'murray@cds.caltech.edu'
24+
MAINTAINER = AUTHOR
25+
MAINTAINER_EMAIL = AUTHOR_EMAIL
26+
URL = 'http://python-control.sourceforge.net'
27+
LICENSE = 'BSD'
28+
DOWNLOAD_URL = URL
29+
VERSION = '0.6e'
30+
PACKAGE_NAME = 'control'
31+
EXTRA_INFO = dict(
32+
install_requires=['scipy', 'matplotlib'],
33+
tests_require=['scipy', 'matplotlib', 'nose']
34+
)
35+
36+
CLASSIFIERS = """\
37+
Development Status :: 3 - Alpha
38+
Intended Audience :: Science/Research
39+
Intended Audience :: Developers
40+
License :: OSI Approved :: BSD License
41+
Programming Language :: Python
42+
Programming Language :: Python :: 3
43+
Topic :: Software Development
44+
Topic :: Scientific/Engineering
45+
Operating System :: Microsoft :: Windows
46+
Operating System :: POSIX
47+
Operating System :: Unix
48+
Operating System :: MacOS
49+
"""
50+
51+
import os
52+
import sys
53+
import subprocess
54+
55+
import setuptools
56+
from numpy.distutils.core import setup
57+
58+
def configuration(parent_package='', top_path=None, package_name=DISTNAME):
59+
if os.path.exists('MANIFEST'): os.remove('MANIFEST')
60+
61+
from numpy.distutils.misc_util import Configuration
62+
config = Configuration(None, parent_package, top_path)
63+
64+
# Avoid non-useful msg: "Ignoring attempt to set 'name' (from ... "
65+
config.set_options(ignore_setup_xxx_py=True,
66+
assume_default_configuration=True,
67+
delegate_options_to_subpackages=True,
68+
quiet=True)
69+
70+
config.add_subpackage(PACKAGE_NAME)
71+
return config
72+
73+
def get_version():
74+
"""Obtain the version number"""
75+
import imp
76+
mod = imp.load_source('version', os.path.join(PACKAGE_NAME, 'version.py'))
77+
return mod.__version__
78+
79+
# Documentation building command
80+
try:
81+
from sphinx.setup_command import BuildDoc as SphinxBuildDoc
82+
class BuildDoc(SphinxBuildDoc):
83+
"""Run in-place build before Sphinx doc build"""
84+
def run(self):
85+
ret = subprocess.call([sys.executable, sys.argv[0], 'build_ext', '-i'])
86+
if ret != 0:
87+
raise RuntimeError("Building Scipy failed!")
88+
SphinxBuildDoc.run(self)
89+
cmdclass = {'build_sphinx': BuildDoc}
90+
except ImportError:
91+
cmdclass = {}
92+
93+
def write_version_py(filename='control/version.py'):
94+
template = """# THIS FILE IS GENERATED FROM THE CONTROL SETUP.PY
95+
version='%s'
96+
"""
97+
cwd = os.path.dirname(__file__)
98+
with open(os.path.join(cwd, filename), 'w') as vfile:
99+
vfile.write(template % VERSION)
100+
101+
# Call the setup function
102+
if __name__ == "__main__":
103+
write_version_py()
104+
105+
setup(configuration=configuration,
106+
name=DISTNAME,
107+
author=AUTHOR,
108+
author_email=AUTHOR_EMAIL,
109+
maintainer=MAINTAINER,
110+
maintainer_email=MAINTAINER_EMAIL,
111+
description=DESCRIPTION,
112+
license=LICENSE,
113+
url=URL,
114+
download_url=DOWNLOAD_URL,
115+
long_description=LONG_DESCRIPTION,
116+
include_package_data=True,
117+
test_suite="nose.collector",
118+
cmdclass=cmdclass,
119+
version=VERSION,
120+
classifiers=[a for a in CLASSIFIERS.split('\n') if a],
121+
**EXTRA_INFO)

0 commit comments

Comments
 (0)