Skip to content

Commit 27446f5

Browse files
Added initial support for ArcGIS Pro 3
1 parent a821621 commit 27446f5

File tree

10 files changed

+135
-64
lines changed

10 files changed

+135
-64
lines changed

arcpyext/_native/_dotnet.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,32 @@
1414

1515
# Standard lib imports
1616
import os
17+
import re
18+
import sys
19+
20+
from decimal import Decimal
1721

1822
# Third-party imports
23+
import arcpy
24+
25+
# clr import, must work out whether we are .NET 6 and setup coreclr first
26+
if sys.version_info[0] >= 3:
27+
arcpy_install_info = arcpy.GetInstallInfo()
28+
major_version = Decimal(re.search(r"^(\d+\.\d+)", arcpy.GetInstallInfo()['Version'], re.IGNORECASE).group(1))
29+
from clr_loader import get_coreclr
30+
from pythonnet import set_runtime
31+
32+
if arcpy_install_info["ProductName"] == "Server" and major_version >= Decimal(11.0):
33+
# running against server, on Python 3, on version 11 or greater
34+
dotnet_config = os.path.join(arcpy_install_info["InstallDir"], r"bin\ArcSOC.runtimeconfig.json")
35+
rt = get_coreclr(dotnet_config)
36+
set_runtime(rt)
37+
elif major_version >= Decimal(3.0):
38+
# if we're not on server, and the product version is 3 or greater (assume pro)
39+
dotnet_config = os.path.join(arcpy_install_info["InstallDir"], r"bin\ArcGISPro.runtimeconfig.json")
40+
rt = get_coreclr(dotnet_config)
41+
set_runtime(rt)
42+
1943
import clr
2044

2145
# .NET Imports
@@ -70,7 +94,6 @@ def release_com_object(self, obj):
7094
def singlethreadapartment(func=None):
7195
def decorator(func):
7296
"""Factory for creating the STA decorator."""
73-
7497
def sta_wrapper(*args, **kwargs):
7598
"""The STA wrapper function."""
7699

@@ -113,8 +136,10 @@ def find_gac_assembly_path(simple_name):
113136
sorted_version_dirs = sorted(os.listdir(simple_assembly_path))
114137
len_sorted_version_dirs = len(sorted_version_dirs)
115138
if len_sorted_version_dirs > 0:
116-
latest_version_dir = os.path.join(simple_assembly_path,
117-
sorted(os.listdir(simple_assembly_path))[len_sorted_version_dirs - 1])
139+
latest_version_dir = os.path.join(
140+
simple_assembly_path,
141+
sorted(os.listdir(simple_assembly_path))[len_sorted_version_dirs - 1]
142+
)
118143
dll_path = os.path.join(latest_version_dir, simple_name + ".dll")
119144
if os.path.isfile(dll_path):
120145
return dll_path

arcpyext/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
__version__ = "0.7.21"
1+
__version__ = "0.7.22"
22
__author__ = "David Whittingham; David Payne; Adam Kerz; Peter Reyne; Daniel Baternik; Chris Blanchfield; Gary Bagnall"
33
__copyright__ = "Copyright (C) 2013-2022 David Whittingham"

arcpyext/mapping/_cim/factories.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,34 @@
99
install_aliases()
1010
# pylint: enable=wildcard-import,unused-wildcard-import,wrong-import-order,wrong-import-position,import-error,no-name-in-module
1111

12+
import json
13+
1214
from .layers import ProFeatureLayer, ProGroupLayer, ProRasterLayer
1315

14-
def create_layer(proj_zip, layer_xml):
15-
"""Factory function for creating a layer object bassed on the input XML."""
16-
# determine XML type
16+
17+
def create_layer(proj_zip, layer_string):
18+
"""Factory function for creating a layer object bassed on the input string."""
19+
# determine layer type, try to pass as JSON first
1720
layer_obj = None
18-
if layer_xml.startswith("<CIMFeatureLayer"):
19-
layer_obj = ProFeatureLayer(proj_zip, layer_xml)
20-
elif layer_xml.startswith("<CIMGroupLayer"):
21-
layer_obj = ProGroupLayer(proj_zip, layer_xml)
22-
elif layer_xml.startswith("<CIMRasterLayer"):
23-
layer_obj = ProRasterLayer(proj_zip, layer_xml)
21+
22+
if layer_string.startswith("{"):
23+
# layer is a JSON string, decode to determine type
24+
25+
layer_json = json.loads(layer_string)
26+
layer_type = layer_json["type"]
27+
if layer_type == "CIMFeatureLayer":
28+
layer_obj = ProFeatureLayer(proj_zip, layer_string)
29+
elif layer_type == "CIMGroupLayer":
30+
layer_obj = ProGroupLayer(proj_zip, layer_string)
31+
elif layer_type == "CIMRasterLayer":
32+
layer_obj = ProRasterLayer(proj_zip, layer_string)
33+
else:
34+
# layer is probably XML, fallback to that
35+
if layer_string.startswith("<CIMFeatureLayer"):
36+
layer_obj = ProFeatureLayer(proj_zip, layer_string)
37+
elif layer_string.startswith("<CIMGroupLayer"):
38+
layer_obj = ProGroupLayer(proj_zip, layer_string)
39+
elif layer_string.startswith("<CIMRasterLayer"):
40+
layer_obj = ProRasterLayer(proj_zip, layer_string)
41+
2442
return layer_obj

arcpyext/mapping/_cim/helpers.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,25 @@
1010
# pylint: enable=wildcard-import,unused-wildcard-import,wrong-import-order,wrong-import-position,import-error,no-name-in-module
1111

1212

13-
def get_xml(zip_file, file_path):
13+
def read_file_in_zip(zip_file, file_path, decode="utf-8"):
1414
"""Reads in an XML file as UTF-8 from a zip file."""
1515
with zip_file.open(file_path) as fp:
16-
return fp.read().decode("utf-8")
16+
if decode:
17+
return fp.read().decode(decode)
18+
else:
19+
return fp.read()
1720

1821

1922
def passthrough_prop(prop_name, doc=None, obj_name="_cim_obj"):
2023
"""Factory function for creating a property that passes through to the underlying ArcGIS Pro SDK object."""
21-
2224
def _get(self):
2325
try:
2426
obj = getattr(self, obj_name)
2527
return getattr(obj, prop_name)
2628
except AttributeError as ae:
27-
raise AttributeError("Unable to get the {} property on this instance of {}.".format(
28-
prop_name, self.__class__.__name__))
29+
raise AttributeError(
30+
"Unable to get the {} property on this instance of {}.".format(prop_name, self.__class__.__name__)
31+
)
2932

3033
def _set(self, val):
3134
raise NotImplementedError("Property {} cannot be set".format(prop_name))

arcpyext/mapping/_cim/layers.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,30 @@ def feature_table(self):
8383

8484

8585
class ProFeatureLayer(ProBasicFeatureLayer):
86-
def __init__(self, proj_zip, xml_string):
87-
super().__init__(proj_zip, CIMFeatureLayer.FromXml(xml_string))
86+
def __init__(self, proj_zip, layer_string):
87+
try:
88+
super().__init__(proj_zip, CIMFeatureLayer.FromXml(layer_string))
89+
except AttributeError:
90+
# probably JSON, attempt that
91+
super().__init__(proj_zip, CIMFeatureLayer.FromJson(layer_string))
8892

8993

9094
class ProRasterLayer(ProLayerBase):
91-
def __init__(self, proj_zip, xml_string):
92-
super().__init__(proj_zip, CIMRasterLayer.FromXml(xml_string))
95+
def __init__(self, proj_zip, layer_string):
96+
try:
97+
super().__init__(proj_zip, CIMRasterLayer.FromXml(layer_string))
98+
except AttributeError:
99+
# probably JSON, attempt that
100+
super().__init__(proj_zip, CIMRasterLayer.FromJson(layer_string))
93101

94102

95103
class ProGroupLayer(ProLayerBase):
96-
def __init__(self, proj_zip, xml_string):
97-
super().__init__(proj_zip, CIMGroupLayer.FromXml(xml_string))
104+
def __init__(self, proj_zip, layer_string):
105+
try:
106+
super().__init__(proj_zip, CIMGroupLayer.FromXml(layer_string))
107+
except AttributeError:
108+
# probably JSON, attempt that
109+
super().__init__(proj_zip, CIMGroupLayer.FromJson(layer_string))
98110

99111
def _get_child_paths(self):
100112
return [cp[8:] for cp in self._cim_obj.Layers]

arcpyext/mapping/_cim/pro_map.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import arcpy
1414

1515
# Local imports
16-
from .helpers import get_xml, passthrough_prop
16+
from .helpers import read_file_in_zip, passthrough_prop
1717
from .factories import create_layer
1818
from .tables import ProStandaloneTable
1919

@@ -27,9 +27,13 @@ class ProMap(object):
2727
_spatial_reference = None
2828
_tables = None
2929

30-
def __init__(self, proj_zip, xml_string):
30+
def __init__(self, proj_zip, map_string):
3131
self._proj_zip = proj_zip
32-
self._cim_obj = CIMMap.FromXml(xml_string)
32+
try:
33+
self._cim_obj = CIMMap.FromXml(map_string)
34+
except AttributeError:
35+
# probably in JSON format, try that
36+
self._cim_obj = CIMMap.FromJson(map_string)
3337

3438
#region PROPERTIES
3539

@@ -40,7 +44,7 @@ def __init__(self, proj_zip, xml_string):
4044
def spatial_reference(self):
4145
if not self._spatial_reference:
4246
self._spatial_reference = arcpy.SpatialReference(self._cim_obj.SpatialReference.Wkid)
43-
47+
4448
return self._spatial_reference
4549

4650
@property
@@ -57,32 +61,31 @@ def layers(self):
5761

5862
# return a shallow copy so our internal list isn't altered
5963
return self._layers.copy()
60-
64+
6165
@property
6266
def tables(self):
6367
if self._tables is None:
6468
self._tables = []
6569

6670
# table paths are pre-pended with 'CIMPATH=', strip that to get the actual zip file path
67-
table_paths = [tp[8:] for tp in self._cim_obj.StandaloneTables]
71+
table_paths = [tp[8:] for tp in (self._cim_obj.StandaloneTables or [])]
6872

6973
# build tables
7074
for tp in table_paths:
7175
# get xml, determine type, create layer object, add to list
72-
table_xml = get_xml(self._proj_zip, tp)
76+
table_string = read_file_in_zip(self._proj_zip, tp)
7377

74-
self._tables.append(ProStandaloneTable(self._proj_zip, table_xml))
75-
76-
return self._tables.copy()
78+
self._tables.append(ProStandaloneTable(self._proj_zip, table_string))
7779

80+
return self._tables.copy()
7881

7982
#endregion
8083

8184
def _create_layers(self, layer_path):
8285
# get xml, determine type, create layer object, add to list
83-
layer_xml = get_xml(self._proj_zip, layer_path)
86+
layer_string = read_file_in_zip(self._proj_zip, layer_path)
8487

85-
layer_obj = create_layer(self._proj_zip, layer_xml)
88+
layer_obj = create_layer(self._proj_zip, layer_string)
8689
self._layers.append(layer_obj)
8790

8891
if layer_obj:

arcpyext/mapping/_cim/pro_project.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from ArcGIS.Core.CIM import CIMGISProject
2020

2121
# Local imports
22-
from .helpers import get_xml
22+
from .helpers import read_file_in_zip
2323
from .pro_map import ProMap
2424

2525

@@ -53,17 +53,23 @@ def maps(self):
5353
map_paths = [pi.CatalogPath[8:] for pi in cimproject_items]
5454

5555
# get XML for each map in archive, create ProMap object
56-
self._pro_maps = [ProMap(self._proj_zip, get_xml(self._proj_zip, map_path)) for map_path in map_paths]
56+
self._pro_maps = [
57+
ProMap(self._proj_zip, read_file_in_zip(self._proj_zip, map_path)) for map_path in map_paths
58+
]
5759

5860
# return a shallow copy so our internal list isn't altered
5961
return self._pro_maps.copy()
6062

6163
@property
6264
def _cimgisproject(self):
63-
if not "GISProject.xml" in self._cims:
64-
self._cims["GISProject.xml"] = CIMGISProject.FromXml(get_xml(self._proj_zip, "GISProject.xml"))
65-
66-
return self._cims["GISProject.xml"]
65+
if not "GISProject" in self._cims:
66+
try:
67+
self._cims["GISProject"] = CIMGISProject.FromXml(read_file_in_zip(self._proj_zip, "GISProject.xml"))
68+
except AttributeError:
69+
# probably running on ArcGIS Pro 3/ArcGIS Server 11, where the CIM on-disk format has moved to JSON, do that instead
70+
self._cims["GISProject"] = CIMGISProject.FromJson(read_file_in_zip(self._proj_zip, "GISProject.json"))
71+
72+
return self._cims["GISProject"]
6773

6874
#endregion
6975

arcpyext/mapping/_mapping.py

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@
3434
# Python-version dependent imports
3535
ARCPY_2 = sys.version_info[0] < 3
3636
if ARCPY_2:
37-
# import to get access to 'private' helper methds
37+
# import to get access to 'private' helper methods
3838
from . import _mapping2 as _mh
3939

4040
# import all 'public' methods into current namespace
4141
from ._mapping2 import *
4242
else:
43-
# import to get access to 'private' helper methds
43+
# import to get access to 'private' helper methods
4444
from . import _mapping3 as _mh
4545

4646
# import all 'public' methods into current namespace
@@ -121,8 +121,9 @@ def change_data_sources(mxd_or_proj, data_sources):
121121

122122
try:
123123
if not len(errors) == 0:
124-
raise ChangeDataSourcesError("A number of errors were encountered whilst change layer data sources.",
125-
errors)
124+
raise ChangeDataSourcesError(
125+
"A number of errors were encountered whilst change layer data sources.", errors
126+
)
126127

127128
if document_was_opened:
128129
# If the document was opened by this function, the map has to be saved for changes to be persisted
@@ -183,8 +184,9 @@ def freeze(d):
183184
def match_new_data_source(layer_or_table):
184185
logger = _get_logger()
185186

186-
if layer_or_table == None or (layer_or_table.get("isGroupLayer") == True
187-
and layer_or_table.get("isNetworkAnalystLayer") != True):
187+
if layer_or_table == None or (
188+
layer_or_table.get("isGroupLayer") == True and layer_or_table.get("isNetworkAnalystLayer") != True
189+
):
188190
# Layers that can't be described or are group layers (except NA layers) can't have their data updated
189191
return None
190192

@@ -208,12 +210,14 @@ def match_new_data_source(layer_or_table):
208210

209211
if tokens["dataSet"] is not None and tokens["schema"] is not None:
210212
logger.debug(1.11)
211-
new_conn["workspacePath"] = "{}\\{}.{}.gdb".format(new_conn["workspacePath"],
212-
tokens["schema"], tokens["dataSet"])
213+
new_conn["workspacePath"] = "{}\\{}.{}.gdb".format(
214+
new_conn["workspacePath"], tokens["schema"], tokens["dataSet"]
215+
)
213216
elif tokens["dataSet"] is not None:
214217
logger.debug(1.12)
215-
new_conn["workspacePath"] = "{}\\{}.gdb".format(new_conn["workspacePath"],
216-
tokens["dataSet"])
218+
new_conn["workspacePath"] = "{}\\{}.gdb".format(
219+
new_conn["workspacePath"], tokens["dataSet"]
220+
)
217221
else:
218222
logger.debug(1.13)
219223
new_conn["workspacePath"] = "{}\\{}.gdb".format(new_conn["workspacePath"], tokens["table"])
@@ -224,10 +228,12 @@ def match_new_data_source(layer_or_table):
224228

225229
return new_conn
226230

227-
return [{
228-
"layers": [match_new_data_source(layer) for layer in df["layers"]],
229-
"tables": [match_new_data_source(table) for table in df["tables"]]
230-
} for df in map_desc["maps"]]
231+
return [
232+
{
233+
"layers": [match_new_data_source(layer) for layer in df["layers"]],
234+
"tables": [match_new_data_source(table) for table in df["tables"]]
235+
} for df in map_desc["maps"]
236+
]
231237

232238

233239
def describe(mxd_or_proj):
@@ -315,14 +321,12 @@ def _match_layers(was_layers, now_layers):
315321
},
316322
{
317323
# same name and id, datasource changed
318-
'fn':
319-
lambda a, b: b
324+
'fn': lambda a, b: b
320325
if same_id(a, b) and same_name(a, b) and not is_resolved_was(a) and not is_resolved_now(b) else None
321326
},
322327
{
323328
# same id and datasource, name changed
324-
'fn':
325-
lambda a, b: b
329+
'fn': lambda a, b: b
326330
if same_id(a, b) and same_datasource(a, b) and not is_resolved_was(a) and not is_resolved_now(b) else None
327331
},
328332
{
@@ -332,8 +336,7 @@ def _match_layers(was_layers, now_layers):
332336
},
333337
{
334338
# same name and datasource, id changed
335-
'fn':
336-
lambda a, b: b
339+
'fn': lambda a, b: b
337340
if same_name(a, b) and same_datasource(a, b) and not is_resolved_was(a) and not is_resolved_now(b) else None
338341
},
339342
{

requirements.test.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
numpy>=1.16,<1.17 ; python_version>='3' # OOTB numpy is broken inside virtual envs for some reason, force install a version for Py3
1+
# numpy>=1.16,<1.17 ; python_version>='3' # OOTB numpy is broken inside virtual envs for some reason, force install a version for Py3
22
pylint==1.9.4 ; python_version<'3'
33
pylint>=2 ; python_version>='3'
44
pytest==4.6.11 ; python_version<'3' # last version that works on Py2

0 commit comments

Comments
 (0)