Skip to content

Commit 1e75cd6

Browse files
committed
Re-worked setup.py to avoid the need for separate/non-standard build commands.
1 parent 07b627b commit 1e75cd6

File tree

3 files changed

+118
-119
lines changed

3 files changed

+118
-119
lines changed

.travis.yml

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,15 +104,6 @@ install:
104104
- echo "[System]" >> $SITE_CFG
105105
- echo "udunits2_path = $PREFIX/lib/libudunits2.so" >> $SITE_CFG
106106

107-
# The coding standards tests expect all the standard names and PyKE
108-
# modules to be present.
109-
- if [[ $TEST_TARGET == 'coding' ]]; then
110-
python setup.py std_names;
111-
PYTHONPATH=lib python setup.py pyke_rules;
112-
fi
113-
114-
# iris
115-
- python setup.py --quiet build
116107
- python setup.py --quiet install
117108

118109
script:

MANIFEST.in

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,18 @@
22
include CHANGES COPYING COPYING.LESSER INSTALL
33

44
# Files from setup.py package_data that are not automatically added to source distributions
5-
recursive-include lib/iris/tests/results *.cml *.cdl *.txt *.xml
5+
recursive-include lib/iris/tests/results *.cml *.cdl *.txt *.xml *.json
66
recursive-include lib/iris/etc *.txt
77
include lib/iris/fileformats/_pyke_rules/*.k?b
88
include lib/iris/tests/stock*.npz
99

1010
# File required to build docs
1111
recursive-include docs Makefile *.js *.png *.py *.rst
12-
recursive-exclude docs/iris/build *
12+
prune docs/iris/build
1313

1414
# Files required to build std_names module
1515
include tools/generate_std_names.py
1616
include etc/cf-standard-name-table.xml
1717

18-
# Extension source
19-
recursive-include src *.c *.cc *.cpp *.h
20-
21-
18+
global-exclude *.pyc
19+
global-exclude __pycache__

setup.py

Lines changed: 114 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
11
from __future__ import print_function
22

3-
import contextlib
4-
from distutils.command import build_ext, build_py
5-
from distutils.core import setup, Command
6-
from distutils.sysconfig import get_config_var
3+
from contextlib import contextmanager
4+
from distutils.core import Command
75
from distutils.util import convert_path
8-
import fnmatch
9-
import multiprocessing
106
import os
7+
from shutil import copyfile
118
import sys
9+
import textwrap
1210

13-
import numpy as np
14-
import setuptools
15-
16-
# Add full path so Python doesn't load any __init__.py in the intervening
17-
# directories, thereby saving setup.py from additional dependencies.
18-
sys.path.append('lib/iris/tests/runner')
19-
from _runner import TestRunner
11+
from setuptools import setup
12+
from setuptools.command.develop import develop as develop_cmd
13+
from setuptools.command.build_py import build_py
2014

2115

2216
# Returns the package and all its sub-packages
@@ -38,7 +32,7 @@ def find_package_tree(root_path, root_package):
3832
if dir_names:
3933
prefix = dir_path.split(os.path.sep)[root_count:]
4034
packages.extend(['.'.join([root_package] + prefix + [dir_name])
41-
for dir_name in dir_names])
35+
for dir_name in dir_names])
4236
return packages
4337

4438

@@ -55,24 +49,34 @@ def file_walk_relative(top, remove=''):
5549
yield os.path.join(root, file).replace(remove, '')
5650

5751

58-
def std_name_cmd(target_dir):
59-
script_path = os.path.join('tools', 'generate_std_names.py')
60-
xml_path = os.path.join('etc', 'cf-standard-name-table.xml')
61-
module_path = os.path.join(target_dir, 'iris', 'std_names.py')
62-
cmd = (sys.executable, script_path, xml_path, module_path)
63-
return cmd
52+
@contextmanager
53+
def temporary_path(directory):
54+
"""
55+
Context manager that adds and subsequently removes the given directory
56+
to sys.path
57+
58+
"""
59+
sys.path.insert(0, directory)
60+
try:
61+
yield
62+
finally:
63+
del sys.path[0]
64+
65+
66+
# Add full path so Python doesn't load any __init__.py in the intervening
67+
# directories, thereby saving setup.py from additional dependencies.
68+
with temporary_path('lib/iris/tests/runner'):
69+
from _runner import TestRunner # noqa:
6470

6571

66-
class SetupTestRunner(TestRunner, setuptools.Command):
72+
class SetupTestRunner(TestRunner, Command):
6773
pass
6874

6975

70-
class CleanSource(Command):
71-
"""
72-
Removes orphaned pyc/pyo files from the sources.
76+
class BaseCommand(Command):
77+
"""A valid no-op command for setuptools & distutils."""
7378

74-
"""
75-
description = 'clean orphaned pyc/pyo files from sources'
79+
description = 'A no-op command.'
7680
user_options = []
7781

7882
def initialize_options(self):
@@ -81,6 +85,13 @@ def initialize_options(self):
8185
def finalize_options(self):
8286
pass
8387

88+
def run(self):
89+
pass
90+
91+
92+
class CleanSource(BaseCommand):
93+
description = 'clean orphaned pyc/pyo files from the source directory'
94+
8495
def run(self):
8596
for root_path, dir_names, file_names in os.walk('lib'):
8697
for file_name in file_names:
@@ -92,92 +103,82 @@ def run(self):
92103
os.remove(compiled_path)
93104

94105

95-
class MakeStdNames(Command):
96-
"""
97-
Generates the CF standard name module containing mappings from
98-
CF standard name to associated metadata.
106+
def compile_pyke_rules(cmd, directory):
107+
# Call out to the python executable to pre-compile the Pyke rules.
108+
# Significant effort was put in to trying to get these to compile
109+
# within this build process but there was no obvious way of finding
110+
# a workaround to the issue presented in
111+
# https://github.com/SciTools/iris/issues/2481.
99112

100-
"""
101-
description = "generate CF standard name module"
102-
user_options = []
113+
shelled_code = textwrap.dedent("""\
103114
104-
def initialize_options(self):
105-
pass
115+
import os
116+
from pyke import knowledge_engine
106117
107-
def finalize_options(self):
108-
pass
118+
# Monkey patch the load method to avoid "ModuleNotFoundError: No module
119+
# named 'iris.fileformats._pyke_rules.compiled_krb'". In this instance
120+
# we simply don't want the knowledge engine, so we turn the load method
121+
# into a no-op.
122+
from pyke.target_pkg import target_pkg
123+
target_pkg.load = lambda *args, **kwargs: None
109124
110-
def run(self):
111-
cmd = std_name_cmd('lib')
112-
self.spawn(cmd)
125+
# Compile the rules by hand, without importing iris. That way we can
126+
# avoid the need for all of iris' dependencies being installed.
127+
os.chdir(os.path.join('{bld_dir}', 'iris', 'fileformats', '_pyke_rules'))
113128
129+
knowledge_engine.engine('')
114130
115-
class MakePykeRules(Command):
116-
"""
117-
Compile the PyKE CF-NetCDF loader rule base.
131+
""".format(bld_dir=directory)).split('\n')
132+
shelled_code = '; '.join(
133+
[line for line in shelled_code
134+
if not line.strip().startswith('#') and line.strip()])
135+
args = [sys.executable, '-c', shelled_code]
136+
cmd.spawn(args)
118137

119-
"""
120-
description = "compile CF-NetCDF loader rule base"
121-
user_options = []
122138

123-
def initialize_options(self):
124-
pass
139+
def copy_copyright(cmd, directory):
140+
# Copy the COPYRIGHT information into the package root
141+
iris_build_dir = os.path.join(directory, 'iris')
142+
for fname in ['COPYING', 'COPYING.LESSER']:
143+
copyfile(fname, os.path.join(iris_build_dir, fname))
125144

126-
def finalize_options(self):
127-
pass
128145

129-
@staticmethod
130-
def _pyke_rule_compile():
131-
"""Compile the PyKE rule base."""
132-
from pyke import knowledge_engine
133-
import iris.fileformats._pyke_rules
134-
knowledge_engine.engine(iris.fileformats._pyke_rules)
146+
def build_std_names(cmd, directory):
147+
# Call out to tools/generate_std_names.py to build std_names module.
135148

136-
def run(self):
137-
# Compile the PyKE rules.
138-
MakePykeRules._pyke_rule_compile()
149+
script_path = os.path.join('tools', 'generate_std_names.py')
150+
xml_path = os.path.join('etc', 'cf-standard-name-table.xml')
151+
module_path = os.path.join(directory, 'iris', 'std_names.py')
152+
args = (sys.executable, script_path, xml_path, module_path)
139153

154+
cmd.spawn(args)
140155

141-
class MissingHeaderError(Exception):
142-
"""
143-
Raised when one or more files do not have the required copyright
144-
and licence header.
145156

157+
def custom_cmd(command_to_override, functions, help_doc=""):
146158
"""
147-
pass
148-
159+
Allows command specialisation to include calls to the given functions.
149160
150-
class BuildPyWithExtras(build_py.build_py):
151161
"""
152-
Adds the creation of the CF standard names module and compilation
153-
of the PyKE rules to the standard "build_py" command.
162+
class ExtendedCommand(command_to_override):
163+
description = help_doc or command_to_override.description
154164

155-
"""
156-
@contextlib.contextmanager
157-
def temporary_path(self):
158-
"""
159-
Context manager that adds and subsequently removes the build
160-
directory to the beginning of the module search path.
161-
162-
"""
163-
sys.path.insert(0, self.build_lib)
164-
try:
165-
yield
166-
finally:
167-
del sys.path[0]
165+
def run(self):
166+
# Run the original command first to make sure all the target
167+
# directories are in place.
168+
command_to_override.run(self)
168169

169-
def run(self):
170-
# Run the main build command first to make sure all the target
171-
# directories are in place.
172-
build_py.build_py.run(self)
170+
# build_lib is defined if we are building the package. Otherwise
171+
# we want to to the work in-place.
172+
dest = getattr(self, 'build_lib', None)
173+
if dest is None:
174+
print(' [Running in-place]')
175+
# Pick the source dir instead (currently in the sub-dir "lib")
176+
dest = 'lib'
173177

174-
# Now build the std_names module.
175-
cmd = std_name_cmd(self.build_lib)
176-
self.spawn(cmd)
178+
for func in functions:
179+
func(self, dest)
177180

178-
# Compile the PyKE rules.
179-
with self.temporary_path():
180-
MakePykeRules._pyke_rule_compile()
181+
return ExtendedCommand
181182

182183

183184
def extract_version():
@@ -193,24 +194,33 @@ def extract_version():
193194
return version
194195

195196

197+
custom_commands = {
198+
'test': SetupTestRunner,
199+
'develop': custom_cmd(
200+
develop_cmd, [build_std_names, compile_pyke_rules]),
201+
'build_py': custom_cmd(
202+
build_py,
203+
[build_std_names, compile_pyke_rules, copy_copyright]),
204+
'std_names':
205+
custom_cmd(BaseCommand, [build_std_names],
206+
help_doc="generate CF standard name module"),
207+
'pyke_rules':
208+
custom_cmd(BaseCommand, [compile_pyke_rules],
209+
help_doc="compile CF-NetCDF loader rules"),
210+
'clean_source': CleanSource,
211+
}
212+
213+
196214
setup(
197215
name='Iris',
198216
version=extract_version(),
199217
url='http://scitools.org.uk/iris/',
200218
author='UK Met Office',
201-
219+
author_email='scitools-iris-dev@googlegroups.com',
202220
packages=find_package_tree('lib/iris', 'iris'),
203221
package_dir={'': 'lib'},
204-
package_data={
205-
'iris': list(file_walk_relative('lib/iris/etc', remove='lib/iris/')) + \
206-
list(file_walk_relative('lib/iris/tests/results',
207-
remove='lib/iris/')) + \
208-
['fileformats/_pyke_rules/*.k?b'] + \
209-
['tests/stock*.npz']
210-
},
211-
data_files=[('iris', ['CHANGES', 'COPYING', 'COPYING.LESSER'])],
222+
include_package_data=True,
212223
tests_require=['nose'],
213-
cmdclass={'test': SetupTestRunner, 'build_py': BuildPyWithExtras,
214-
'std_names': MakeStdNames, 'pyke_rules': MakePykeRules,
215-
'clean_source': CleanSource},
216-
)
224+
cmdclass=custom_commands,
225+
zip_safe=False,
226+
)

0 commit comments

Comments
 (0)