11from __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
75from distutils .util import convert_path
8- import fnmatch
9- import multiprocessing
106import os
7+ from shutil import copyfile
118import 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
183184def 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+
196214setup (
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