├── .gitignore ├── pytexportutils ├── setup.py └── pytexportutils │ └── __init__.py ├── converttbx.pyt ├── README.md └── tbxtopyt.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | -------------------------------------------------------------------------------- /pytexportutils/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from distutils.core import setup, Extension 4 | 5 | import arcpy 6 | 7 | install_dir = arcpy.GetInstallInfo()['InstallDir'] 8 | com_dir = os.path.join(install_dir, 'com') 9 | 10 | setup( 11 | name='pytexportutils', 12 | version='10.2', 13 | packages=['pytexportutils'], 14 | ext_package='pytexportutils', 15 | ext_modules=[ 16 | Extension('_esriSystem', 17 | ['src/esriSystem.cpp'], 18 | include_dirs=[com_dir]), 19 | Extension('_esriGeometry', 20 | ['src/esriGeometry.cpp'], 21 | include_dirs=[com_dir]), 22 | Extension('_esriGeoDatabase', 23 | ['src/esriGeoDatabase.cpp'], 24 | include_dirs=[com_dir]), 25 | Extension('_esriGeoprocessing', 26 | ['src/esriGeoprocessing.cpp'], 27 | include_dirs=[com_dir]) 28 | ], 29 | package_data={'pytexportutils': ["./*.pdb"]} 30 | ) 31 | -------------------------------------------------------------------------------- /pytexportutils/pytexportutils/__init__.py: -------------------------------------------------------------------------------- 1 | """Type library collection pytexportutils""" 2 | __version__ = '10.2' 3 | __all__ = ['esriSystem', 'esriGeometry', 'esriGeoDatabase', 'esriGeoprocessing'] 4 | # Required by all submodules, if these get in a bad state the C modules will crash. 5 | _IIDMap = {} 6 | _CLSIDMap = {} 7 | _RecordMap = {} 8 | 9 | class Enumeration(object): 10 | "Base class for enumerations" 11 | @classmethod 12 | def value_for(cls, value): 13 | """Look up the textual name for an integer representation of an 14 | enumeration value""" 15 | return ([x for x in cls.__slots__ 16 | if getattr(cls, x) == value] or [None]).pop() 17 | __slots__ = [] 18 | pass 19 | 20 | 21 | def IndexProperty(getter=None, setter=None): 22 | "For getter/setters with an index argument" 23 | class IndexedPropertyGetter(object): 24 | def __init__(self, other): 25 | self._other = other 26 | def __setitem__(self, index, value): 27 | if setter is not None: 28 | return setter(self._other, index, value) 29 | raise TypeError("%s object does not support item assignment" % 30 | self._other.__class__.__name__) 31 | def __getitem__(self, index): 32 | if getter is not None: 33 | return getter(self._other, index) 34 | raise TypeError("%s object does not support indexing" % 35 | self._other.__class__.__name__) 36 | return property(IndexedPropertyGetter) 37 | 38 | 39 | def FAILED(item): 40 | """Usage: FAILED(HRESULT or IFace) 41 | 42 | Indicates if the specified HRESULT indicates a failure or the last call 43 | on the specified interface instance failed.""" 44 | hr = item 45 | if not isinstance(item, (int, long)): 46 | if hasattr(item, '_HR'): 47 | hr = item._HR 48 | if not isinstance(hr, (int, long)): 49 | raise ValueError(repr(hr)) 50 | return bool(hr & 0x80000000) 51 | 52 | def SUCCEEDED(item): 53 | """SUCCEEDED(HRESULT or IFace) 54 | 55 | Indicates if the specified HRESULT indicates a failure or the last call 56 | on the specified interface instance succeeded.""" 57 | return not FAILED(item) 58 | 59 | def interfaces_supported(interface_object): 60 | """Returns a list of Interface types in this packages supported by the 61 | supplied COM object instance.""" 62 | if not hasattr(interface_object, 'supports'): 63 | interface_object = IUnknown(interface_object) # coerce 64 | return [iface for iid, iface in _IIDMap.iteritems() 65 | if interface_object.supports(iid)] 66 | 67 | from pytexportutils.esriSystem import * 68 | from pytexportutils.esriGeometry import * 69 | from pytexportutils.esriGeoDatabase import * 70 | from pytexportutils.esriGeoprocessing import * 71 | -------------------------------------------------------------------------------- /converttbx.pyt: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import contextlib 4 | import os 5 | import sys 6 | 7 | import arcpy 8 | 9 | @contextlib.contextmanager 10 | def script_run_as(filename, args=None): 11 | oldpath = sys.path[:] 12 | oldargv = sys.argv[:] 13 | newdir = os.path.dirname(filename) 14 | sys.path = oldpath + [newdir] 15 | sys.argv = [filename] + [arg.valueAsText for arg in (args or [])] 16 | oldcwd = os.getcwdu() 17 | os.chdir(newdir) 18 | 19 | try: 20 | # Actually run 21 | yield filename 22 | finally: 23 | # Restore old settings 24 | sys.path = oldpath 25 | sys.argv = oldargv 26 | os.chdir(oldcwd) 27 | 28 | class Toolbox(object): 29 | def __init__(self): 30 | self.label = u'Convert to PYT' 31 | self.alias = u'pytmaker' 32 | self.tools = [CreatePYT] 33 | 34 | # Tool implementation code 35 | 36 | class CreatePYT(object): 37 | def __init__(self): 38 | self.label = u'Create PYT Template from TBX' 39 | self.description = u'Create a new PYT skeleton from the structure and parameters of an ArcGIS Toolbox (TBX) file.' 40 | self.canRunInBackground = False 41 | def getParameterInfo(self): 42 | # Input_Toolbox 43 | param_1 = arcpy.Parameter() 44 | param_1.name = u'Input_Toolbox' 45 | param_1.displayName = u'Input Toolbox' 46 | param_1.parameterType = 'Required' 47 | param_1.direction = 'Input' 48 | param_1.datatype = u'Toolbox' 49 | 50 | # Output_File 51 | param_2 = arcpy.Parameter() 52 | param_2.name = u'Output_File' 53 | param_2.displayName = u'Output File' 54 | param_2.parameterType = 'Required' 55 | param_2.direction = 'Output' 56 | param_2.datatype = u'File' 57 | 58 | return [param_1, param_2] 59 | def isLicensed(self): 60 | return True 61 | def updateParameters(self, parameters): 62 | if parameters[0].valueAsText and (not (parameters[1].valueAsText or parameters[1].altered)): 63 | if os.path.isdir(os.path.dirname(parameters[0].valueAsText)): 64 | newnamepart = os.path.splitext(parameters[0].valueAsText)[0] 65 | newnamepart += u"_converted.pyt" 66 | parameters[1].value = newnamepart 67 | elif arcpy.env.workspace and os.path.isdir(arcpy.env.workspace): 68 | newnamepart = os.path.splitext(os.path.split(parameters[0].valueAsText)[1])[0] 69 | newname = os.path.join(arcpy.env.workspace, newnamepart + "_converted.pyt") 70 | else: 71 | newnamepart = os.path.splitext(os.path.split(parameters[0].valueAsText)[1])[0] 72 | newname = os.path.join(os.getcwdu(), newnamepart + "_converted.pyt") 73 | def updateMessages(self, parameters): 74 | pass 75 | def execute(self, parameters, messages): 76 | with script_run_as(__file__): 77 | import tbxtopyt 78 | if not parameters[1].valueAsText.lower().endswith('.pyt'): 79 | parameters[1].value = parameters[1].valueAsText + ".pyt" 80 | tbxtopyt.export_tbx_to_pyt(parameters[0].valueAsText, parameters[1].valueAsText) 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TBX to PYT Translator 2 | 3 | This Python toolbox (converttbx.pyt) will take any geoprocessing 4 | toolbox file (.TBX) and create a corresponding stub .PYT with a 5 | corresponding Python implementation of the tools with the original 6 | parameters of original toolbox. 7 | 8 | ## Features 9 | * Create Skeleton PYT from a TBX 10 | * Basic conversion from geoprocessing toolbox (.tbx) to Python toolbox (.pyt). 11 | 12 | ## Requirements 13 | 14 | * ArcGIS 10.1 15 | * Some experience editing Python code 16 | * Microsoft Visual Studio 2008 or [Microsoft Visual C++ Compiler for Python 2.7](http://www.microsoft.com/en-us/download/details.aspx?id=44266) (to compile the C extensions yourself if you go the build route) 17 | 18 | ## Instructions for Downloading (**recommended method**) 19 | 20 | 1. Download the [pre-built version from ArcGIS.com](http://www.arcgis.com/home/item.html?id=83585412edd04ae48bdffea3e1f7b2e7) and continue with the steps below for usage. 21 | 22 | ## Instructions for Building 23 | 24 | 1. Download and unzip the .zip file or clone the repo. 25 | 2. Build and install `pytexportutils`: `C:\Python27\ArcGIS10.2\python setup.py install`. 26 | 3. Continue with the instructions for using the toolbox. 27 | 28 | ## Instructions for Using (after downloading or building) 29 | 30 | 1. Open the provided `converttbx.pyt` inside of ArcCatalog or Catalog View in ArcMap. 31 | 2. Provide the existing .tbx file as input. 32 | 3. Examine and refine the resulting `.pyt` file. 33 | 34 | [New to Github? Get started here.](https://github.com/) 35 | 36 | ## Resources 37 | 38 | * [Python for ArcGIS Resource Center](http://resources.arcgis.com/en/communities/python/) 39 | * [Analysis and Geoprocessing Blog](http://blogs.esri.com/esri/arcgis/category/subject-analysis-and-geoprocessing/) 40 | * [twitter@arcpy](http://twitter.com/arcpy) 41 | 42 | ## Issues 43 | 44 | Find a bug or want to request a new feature? Please let us know by submitting an issue. 45 | 46 | # ! WARNING ! 47 | 48 | THIS IS NOT A 100% AUTOMATED SOLUTION TO CREATING PYTS. You will 49 | need to go in and look over the source before you use it. There 50 | will be areas where you NEED to change the source of the new PYT, 51 | and others where you'll need to do some sanity checking to make 52 | sure the PYT's functionality is similar to your original TBX. 53 | 54 | 55 | ## Contributing 56 | 57 | Anyone and everyone is welcome to contribute. 58 | 59 | ## Licensing 60 | Copyright 2012 Esri 61 | 62 | Licensed under the Apache License, Version 2.0 (the "License"); 63 | you may not use this file except in compliance with the License. 64 | You may obtain a copy of the License at 65 | 66 | http://www.apache.org/licenses/LICENSE-2.0 67 | 68 | Unless required by applicable law or agreed to in writing, software 69 | distributed under the License is distributed on an "AS IS" BASIS, 70 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 71 | See the License for the specific language governing permissions and 72 | limitations under the License. 73 | 74 | A copy of the license is available in the repository's [license.txt](https://raw.github.com/Esri/switch-basemaps-js/master/license.txt) file. 75 | 76 | [](Esri Tags: ArcGIS Toolboxes) 77 | [](Esri Language: Python) 78 | -------------------------------------------------------------------------------- /tbxtopyt.py: -------------------------------------------------------------------------------- 1 | import arcpy 2 | 3 | import imp 4 | import os 5 | import re 6 | 7 | __all__ = ['export_tbx_to_pyt'] 8 | 9 | mod_find = imp.find_module('pytexportutils', [os.path.abspath(os.path.dirname(__file__))]) 10 | pytexportutils = imp.load_module('pytexportutils', *mod_find) 11 | 12 | ACCEPTABLE_VARIABLENAME = re.compile("^[_a-z][_a-z0-9]*$", re.IGNORECASE) 13 | CALL_RE_TEMPLATE = "((?:[_a-z][_a-z0-9]* *[.] *)*{}\(([^)]*)\))" 14 | CODING_RE = re.compile("coding: ([^ ]+)", re.IGNORECASE) 15 | HEADER_SOURCE = """# -*- coding: utf-8 -*- 16 | 17 | import contextlib 18 | import os 19 | import sys 20 | 21 | import arcpy 22 | 23 | # You can ignore/delete this code; these are basic utility functions to 24 | # streamline porting 25 | 26 | @contextlib.contextmanager 27 | def script_run_as(filename, args=None): 28 | oldpath = sys.path[:] 29 | oldargv = sys.argv[:] 30 | newdir = os.path.dirname(filename) 31 | sys.path = oldpath + [newdir] 32 | sys.argv = [filename] + [arg.valueAsText for arg in (args or [])] 33 | oldcwd = os.getcwdu() 34 | os.chdir(newdir) 35 | 36 | try: 37 | # Actually run 38 | yield filename 39 | finally: 40 | # Restore old settings 41 | sys.path = oldpath 42 | sys.argv = oldargv 43 | os.chdir(oldcwd) 44 | 45 | def set_parameter_as_text(params, index, val): 46 | if (hasattr(params[index].value, 'value')): 47 | params[index].value.value = val 48 | else: 49 | params[index].value = val 50 | """ 51 | 52 | FUNCTION_REMAPPINGS = ( 53 | ('AddMessage', 'messages.AddMessage({})'), 54 | ('AddWarning', 'messages.AddWarningMessage({})'), 55 | ('AddError', 'messages.AddErrorMessage({})'), 56 | ('AddIDMessage', 'messages.AddIDMessage({})'), 57 | ('GetParameterAsText', 'parameters[{}].valueAsText'), 58 | ('SetParameterAsText', 'set_parameter_as_text(parameters, {})'), 59 | ('GetParameter', 'parameters[{}]'), 60 | ('GetArgumentCount', 'len(parameters)'), 61 | ('GetParameterInfo', 'parameters') 62 | ) 63 | 64 | def collect_lines(fn): 65 | def fn_(*args, **kws): 66 | return '\n'.join(fn(*args, **kws)) 67 | return fn_ 68 | 69 | def rearrange_source(source, indentation): 70 | source = source.replace("\r\n", "\n").replace("\t", " ") 71 | # Guess coding? 72 | if '\n' in source: 73 | id1 = source.find('\n') 74 | if '\n' in source[id1+1:]: 75 | id1 = source.find('\n', id1 + 1) 76 | match_obj = CODING_RE.findall(source[:id1]) 77 | if match_obj: 78 | source = source.decode(match_obj[0]) 79 | else: 80 | try: 81 | source = source.decode("utf-8") 82 | except: 83 | pass 84 | src = "\n".join("{}{}".format(" " * indentation, 85 | line.encode("utf-8")) 86 | for line in source.split("\n")) 87 | # Now apply some mechanical translations to common parameter access routines 88 | for fnname, replacement_pattern in FUNCTION_REMAPPINGS: 89 | regexp = re.compile(CALL_RE_TEMPLATE.format(fnname), re.IGNORECASE) 90 | finds = regexp.findall(src) 91 | if finds: 92 | for codepattern, arguments in finds: 93 | src = src.replace(codepattern, replacement_pattern.format(arguments)) 94 | return src 95 | 96 | class Tool(object): 97 | def __init__(self, tool_object): 98 | self._tool = tool_object 99 | param_list = self._tool.ParameterInfo 100 | self._parameters = [pytexportutils.IGPParameter(param_list.Element[idx]) 101 | for idx in xrange(param_list.Count)] 102 | @property 103 | def name(self): 104 | try: 105 | if ACCEPTABLE_VARIABLENAME.match(self._tool.Name): 106 | return self._tool.Name 107 | except: 108 | pass 109 | return "Tool{}".format(hex(id(self))[2:]) 110 | @property 111 | @collect_lines 112 | def python_code(self): 113 | yield "class {}(object):".format(self.name) 114 | yield ' """{}"""'.format(self._tool.PathName.encode("utf-8")) 115 | 116 | try: 117 | gptool = pytexportutils.IGPScriptTool2(self._tool) 118 | codeblock = gptool.CodeBlock.encode("utf-8").replace("__init__(self)", "__init__(self, parameters)") 119 | yield rearrange_source(codeblock, 4) 120 | except: 121 | pass 122 | 123 | yield " def __init__(self):" 124 | try: 125 | yield " self.label = {}".format(repr(self._tool.DisplayName)) 126 | except: 127 | pass 128 | try: 129 | yield " self.description = {}".format(repr(self._tool.Description)) 130 | except: 131 | pass 132 | try: 133 | yield " self.canRunInBackground = {}".format(not pytexportutils.IGPScriptTool2(self._tool).RunInProc) 134 | except: 135 | yield " self.canRunInBackground = False" 136 | yield " def getParameterInfo(self):" 137 | index_dict = {parameter.Name.lower(): idx for idx, parameter in enumerate(self._parameters)} 138 | for idx, parameter in enumerate(self._parameters): 139 | yield " # {}".format(parameter.Name.encode("utf-8")) 140 | yield " param_{} = arcpy.Parameter()".format(idx + 1) 141 | yield " param_{}.name = {}".format(idx + 1, repr(parameter.Name)) 142 | yield " param_{}.displayName = {}".format(idx + 1, repr(parameter.DisplayName)) 143 | yield " param_{}.parameterType = {}".format(idx + 1, 144 | repr(pytexportutils.esriGPParameterType.value_for(parameter.ParameterType) 145 | [len('esriGPParameterType'):])) 146 | yield " param_{}.direction = '{}'".format(idx + 1, 147 | "Output" if (parameter.Direction == 148 | pytexportutils.esriGPParameterDirection 149 | .esriGPParameterDirectionOutput) else "Input") 150 | if (parameter.DataType.supports(pytexportutils.IGPMultiValueType._IID)): 151 | yield " param_{}.datatype = {}".format(idx + 1, repr(pytexportutils.IGPMultiValueType(parameter.DataType).MemberDataType.Name)) 152 | yield " param_{}.multiValue = True".format(idx + 1) 153 | elif (parameter.DataType.supports(pytexportutils.IGPCompositeDataType._IID)): 154 | cv = pytexportutils.IGPCompositeDataType(parameter.DataType) 155 | yield " param_{}.datatype = {}".format(idx + 1, repr(tuple(cv.DataType[x].Name for x in xrange(cv.Count)))) 156 | elif (parameter.DataType.supports(pytexportutils.IGPValueTableType._IID)): 157 | vt = pytexportutils.IGPValueTableType(parameter.DataType) 158 | tablecols = [(vt.DataType[colindex].Name, vt.DisplayName[colindex]) for colindex in xrange(vt.Count)] 159 | yield " param_{}.columns = {}".format(idx + 1, repr(tablecols)) 160 | else: 161 | yield " param_{}.datatype = {}".format(idx + 1, repr(parameter.DataType.Name)) 162 | # default value 163 | try: 164 | value = parameter.Value.GetAsText() 165 | if value: 166 | yield " param_{}.value = {}".format(idx + 1, repr(value)) 167 | except: 168 | pass 169 | 170 | # .filter.list 171 | try: 172 | cvd = pytexportutils.IGPCodedValueDomain(parameter.Domain) 173 | cvd_list = [cvd.Value[domainidx].GetAsText() for domainidx in xrange(cvd.CodeCount)] 174 | yield " param_{}.filter.list = {}".format(idx + 1, repr(cvd_list)) 175 | except: 176 | pass 177 | 178 | # .parameterDependencies 179 | try: 180 | deps = parameter.ParameterDependencies 181 | keep_going = True 182 | dep_list = [] 183 | try: 184 | while keep_going: 185 | dep_list.append(index_dict.get(deps.Next().lower(), 0)) 186 | if not dep_list[-1]: 187 | keep_going = False 188 | dep_list = dep_list[:-1] 189 | except: 190 | pass 191 | if dep_list: 192 | yield " param_{}.parameterDependencies = {}".format(idx + 1, repr(dep_list)) 193 | except: 194 | pass 195 | yield "" 196 | yield " return [{}]".format(", ".join("param_{}".format(idx + 1) for idx in xrange(len(self._parameters)))) 197 | yield " def isLicensed(self):" 198 | yield " return True" 199 | yield " def updateParameters(self, parameters):" 200 | yield " validator = getattr(self, 'ToolValidator', None)" 201 | yield " if validator:" 202 | yield " return validator(parameters).updateParameters()" 203 | yield " def updateMessages(self, parameters):" 204 | yield " validator = getattr(self, 'ToolValidator', None)" 205 | yield " if validator:" 206 | yield " return validator(parameters).updateMessages()" 207 | yield " def execute(self, parameters, messages):" 208 | try: 209 | filename = pytexportutils.IGPScriptTool(self._tool).FileName 210 | if os.path.isfile(filename): 211 | file_contents = open(filename, 'rb').read() 212 | yield " with script_run_as({}):".format(repr(filename)) 213 | yield rearrange_source(file_contents, 12) 214 | else: 215 | yield " # {}".format(repr(filename)) 216 | try: 217 | # Coerce as source? 218 | compile(filename, self._tool.PathName, "exec") 219 | yield " with script_run_as({}):".format(repr(self._tool.PathName)) 220 | yield rearrange_source(filename, 12) 221 | except: 222 | pass 223 | except: 224 | yield " pass" 225 | 226 | class PYTToolbox(object): 227 | def __init__(self, tbx_name): 228 | gpu = pytexportutils.IGPUtilities(pytexportutils.GPUtilities()) 229 | name = gpu.GetNameObjectFromLocation(tbx_name) 230 | assert name, "Could not find {}".format(tbx_name) 231 | new_object = name.Open() 232 | assert new_object, "could not open toolbox" 233 | assert new_object.supports(pytexportutils.IGPToolbox._IID), "{} is not a toolbox".format(tbx_name) 234 | self._toolbox = pytexportutils.IGPToolbox(new_object) 235 | self._tools = map(Tool, iter(self._toolbox.Tools.Next, None)) 236 | @property 237 | @collect_lines 238 | def python_code(self): 239 | yield "# Export of toolbox {}".format(self._toolbox.PathName.encode("utf-8")) 240 | yield "" 241 | yield "import arcpy" 242 | yield "" 243 | yield "class Toolbox(object):" 244 | yield " def __init__(self):" 245 | tbx_name = os.path.splitext(os.path.basename(self._toolbox.PathName))[0] 246 | try: 247 | tbx_name = pytexportutils.IGPToolbox2(self._toolbox).DisplayName 248 | except: 249 | pass 250 | yield " self.label = {}".format(repr(tbx_name)) 251 | alias = "" 252 | try: 253 | alias = self._toolbox.Alias 254 | except: 255 | pass 256 | yield " self.alias = {}".format(repr(alias)) 257 | yield " self.tools = [{}]".format(", ".join(t.name for t in self._tools)) 258 | yield "" 259 | yield "# Tool implementation code" 260 | for tool in self._tools: 261 | yield "" 262 | yield tool.python_code 263 | 264 | def export_tbx_to_pyt(in_tbx, out_file): 265 | toolbox = PYTToolbox(in_tbx) 266 | with open(out_file, 'wb') as out: 267 | out.write(HEADER_SOURCE) 268 | out.write('\n') 269 | out.write(toolbox.python_code) 270 | 271 | if __name__ == "__main__": 272 | import glob 273 | #[r'C:\SupportFiles\ArcGIS\ArcToolBox\Toolboxes\Spatial Statistics Tools.tbx']: #[r'Toolboxes\My Toolboxes\OutScript.tbx']: 274 | for filename in [os.path.join(os.path.dirname(os.path.abspath(__file__)), "dashboard.tbx")]: 275 | print HEADER_SOURCE 276 | print PYTToolbox(filename).python_code.encode("ascii", "replace") 277 | --------------------------------------------------------------------------------