I am migrating a script tool to client's machine. It was working in my local environment (Pro 3.5.2 against fgdb), but is now failing in the destination environment (Pro 3.3.1, Traditionally versioned SDE 11.2). The tool reverses a singlepart line using `arcpy.edit.flipline()`, before applying other attribute edits to adjacent features. This worked well in a file geodatabase but is now silently failing to flip the line in the destination environment (the whole script executes without errors but fails to flip the line).
I have tried manually running the Flip Line geoprocessing tool in the destination environment and that worked. I have also tried the following changes to the script without success in the destination environment:
- toggling multiuser mode boolean when initializing the edit session
- recreating the flipline function from scratch
Here's 2 versions of the line reversal logic (both of which fail silently in the destination env):
def reverse_pipe_direction(SewerLine):
pipe_count = int(arcpy.management.GetCount(SewerLine)[0])
if pipe_count != 1:
arcpy.AddError(f"Error: This tool can only run on a single pipe at a time. {pipe_count} features were selected.")
raise Exception("Multiple or zero features selected.")
assetID = ''
with arcpy.da.SearchCursor(SewerLine, "assetid") as cursor:
for row in cursor:
assetID = row[0]
arcpy.AddMessage("Reversing Pipe ID: " + assetID)
# Process: Flip Line (edit)
arcpy.edit.FlipLine(in_features=SewerLine)[0]
return SewerLine
def reverse_pipe_direction(SewerLine):
"""Reverses the direction of each single-part polyline in the input feature class"""
pipe_count = int(arcpy.management.GetCount(SewerLine)[0])
if pipe_count != 1:
arcpy.AddError(f"Error: This tool can only run on a single pipe at a time. {pipe_count} features were selected.")
raise Exception("Multiple or zero features selected.")
edit = arcpy.da.Editor(arcpy.env.workspace, multiuser_mode=True)
edit.startEditing()
edit.startOperation()
with arcpy.da.UpdateCursor(SewerLine, ["SHAPE@", "assetid"]) as cursor:
for row in cursor:
arcpy.AddMessage(f"Reversing pipe with AssetID {row[1]}")
points = [pt for pt in row[0].getPart()]
flipped_points = arcpy.Array(reversed(points))
flipped_line = arcpy.Polyline(flipped_points, row[0].spatialReference)
cursor.updateRow((flipped_line, row[1]))
edit.stopOperation()
edit.stopEditing(True)
return SewerLine
Thanks in advance for any pointers!
Source System info:
- ArcGIS Pro 3.5.2 against local File GDB
Destination System Info:
- ArcGIS Pro 3.3.1 against Enterprise DB
- SDE Version 11.2.0 (described as an ArcGIS Pro 3.2.0 - 11.2.0.49743 geodatabase in the Database Properties)
- DB uses Traditional Versioning
Just in case you aren't aware, a `reverseOrientation` method has been added to Polyline that does this. It returns a new Polyline object as well so you can have both orientations of the line!
I'd also use the Editor context manager,
with Editor(...) as edit:
...
Make sure you're setting the correct flags as well, with_undo and multiuser should both be True.
Edit: One last thing to check, have you validated that the arcpy.env.workspace global is returning the correct workspace? If not, you'll need to extract the workspace from your feature path/layer/class. Either by pulling it out of the path string, using Describe, or getting the workspace attribute.
Edit2: Got to a computer and bashed this out to explain:
from arcpy import AddError, AddMessage, Polyline, Array
from arcpy.da import (
SearchCursor,
UpdateCursor,
Editor,
)
from arcpy._mp import Layer
def reverse_pipe_direction(sewer_layer: Layer):
# The getCount function is kinda slow and I don't like that it has a weird return type
# I tend to use a SearchCursor for figuring out the size of a table. Added benefit of being
# able to break out of the cursor early if you know you need a certain number of records
# This will get the length of the selected features
# (assuming that `sewer_layer` is a Layer and not a feature path)
with SearchCursor(sewer_layer, ['OID@']) as cur:
# You could also break out of this if you see more than one pipe
# and save on the cost of traversing the whole table
pipe_count = sum(1 for _ in cur)
# Enforce single pipe rule and warn user for bad input
if pipe_count > 1:
AddError(
"Error: This tool can only run on a single pipe at a time."
f"{pipe_count} features were selected."
)
raise ValueError('Multiple Sewer Line features selected!')
if pipe_count == 0:
AddError(
"Error: This tool can only run on a single pipe at a time."
"No features were selected."
)
raise ValueError('No Sewer Line features selected!')
# Update `sewer_layer.dataSource` with the actual workspace for your layer
# Inline with to de-nest the dual contexts
with Editor(sewer_layer.dataSource, multiuser_mode=True), UpdateCursor(sewer_layer, ['SHAPE@', 'assetid']) as cursor:
for shape, asset_id in cursor:
AddMessage(f'Reversing pipe with AssetID {asset_id}')
# This is to force the type of shape to be a Polyline for mypy/typechecking
if not isinstance(shape, Polyline): continue
# Use new reverseOrientation if available
if hasattr(shape, 'reverseOrientation'):
flipped_line = shape.reverseOrientation()
else:
# reversed() is lazy, I think the Array initializer expects a list
flipped_line = Polyline(Array([pt for pt in shape.getPart()][::-1]), spatial_reference=shape.spatialReference)
# Update the record with the flipped geometry
cursor.updateRow([flipped_line, asset_id])
# Return the original layer for additional processing
# You could also return the asset id of the pipe here?
return sewer_layer
try something like this, it's untested.
import arcpy
def flip_line_using_gp_tool(input_features):
"""Flips lines using the Flip Line geoprocessing tool."""
# Check if input is a feature layer or path
if isinstance(input_features, str):
input_path = input_features
else:
input_path = input_features.dataSource # Extract path from layer object
# Use a temporary feature class to avoid editing the original directly
temp_fc = arcpy.management.CopyFeatures(input_features, "in_memory/temp_flip")[0]
try:
# Call the Flip Line tool (returns a Result object)
flip_result = arcpy.edit.FlipLine(temp_fc)
# Execute flip and show messages
arcpy.AddMessage(f"Flip Line tool messages: {flip_result.getMessages()}")
# Overwrite the original feature with the flipped geometry
with arcpy.da.UpdateCursor(input_path, ["SHAPE@"]) as update_cursor, \
arcpy.da.SearchCursor(flip_result[0], ["SHAPE@"]) as search_cursor:
for (original_row, flipped_row) in zip(update_cursor, search_cursor):
original_row[0] = flipped_row[0] # Replace geometry
update_cursor.updateRow(original_row)
finally:
# Clean up temporary data
arcpy.management.Delete(temp_fc)
With edit sessions use,
with arcpy.da.Editor(arcpy.env.workspace, multiuser_mode=True):
arcpy.edit.FlipLine(in_features=SewerLine)[0]