@@ -985,7 +985,7 @@ def convertDataRole(self, dataRole):
985
985
return conversion
986
986
987
987
@classmethod
988
- def createRequirementsJSON (cls , jPath = Path .cwd ()):
988
+ def create_requirements_json (cls , json_path = Path .cwd ()):
989
989
"""
990
990
Searches the model directory for Python scripts and pickle files and determines
991
991
their Python package dependencies. Found dependencies are then matched to the package
@@ -999,51 +999,50 @@ def createRequirementsJSON(cls, jPath=Path.cwd()):
999
999
1000
1000
This function works best when run in the model development environment and is likely to
1001
1001
throw errors if run in another environment (and/or produce incorrect package versions).
1002
- In the case of using this function outside of the model development environment, it is
1002
+ In the case of using this function outside the model development environment, it is
1003
1003
recommended to the user that they adjust the requirements.json file's package versions
1004
1004
to match the model development environment.
1005
1005
1006
1006
Parameters
1007
1007
----------
1008
- jPath : str, optional
1009
- The path to a Python project, by default Path.cwd().
1010
-
1008
+ json_path : str, optional
1009
+ The path to a Python project, by default the current working directory.
1011
1010
Yields
1012
1011
------
1013
1012
requirements.json : file
1014
1013
JSON file used to create a specific Python environment in a SAS Model Manager published
1015
1014
container.
1016
1015
"""
1017
1016
1018
- picklePackages = []
1019
- pickleFiles = cls .getPickleFile ( jPath )
1020
- for pickleFile in pickleFiles :
1021
- picklePackages .append (cls .getDependenciesFromPickleFile ( cls , pickleFile ))
1017
+ pickle_packages = []
1018
+ pickle_files = cls .get_pickle_file ( json_path )
1019
+ for pickle_file in pickle_files :
1020
+ pickle_packages .append (cls .get_pickle_dependencies ( pickle_file ))
1022
1021
1023
- codeDependencies = cls .getCodeDependencies ( cls , jPath )
1022
+ code_dependencies = cls .get_code_dependencies ( json_path )
1024
1023
1025
- packageList = list (picklePackages ) + codeDependencies
1026
- packageList = list (set (list (flatten (packageList ))))
1027
- packageList = cls .removeStdlibPackages ( packageList )
1028
- packageAndVersion = cls .getLocalPackageVersion ( packageList )
1024
+ package_list = list (pickle_packages ) + code_dependencies
1025
+ package_list = list (set (list (flatten (package_list ))))
1026
+ package_list = cls .remove_standard_library_packages ( package_list )
1027
+ package_and_version = cls .get_local_package_version ( package_list )
1029
1028
# Identify packages with missing versions
1030
- missingPackageVersions = [item [0 ] for item in packageAndVersion if not item [1 ]]
1029
+ missing_package_versions = [item [0 ] for item in package_and_version if not item [1 ]]
1031
1030
1032
- with open (Path (jPath ) / "requirements.json" , "w" ) as file :
1033
- if missingPackageVersions :
1034
- jsonStep = json .dumps (
1031
+ with open (Path (json_path ) / "requirements.json" , "w" ) as file :
1032
+ if missing_package_versions :
1033
+ json_step = json .dumps (
1035
1034
[
1036
1035
{
1037
1036
"Warning" : "The versions for the following packages could not be determined:" ,
1038
- "Packages" : ", " .join (missingPackageVersions )
1037
+ "Packages" : ", " .join (missing_package_versions )
1039
1038
}
1040
1039
],
1041
1040
indent = 4 ,
1042
1041
)
1043
- file .write (jsonStep )
1044
- for package , version in packageAndVersion :
1042
+ file .write (json_step )
1043
+ for package , version in package_and_version :
1045
1044
if version :
1046
- jsonStep = json .dumps (
1045
+ json_step = json .dumps (
1047
1046
[
1048
1047
{
1049
1048
"step" : "install " + package ,
@@ -1053,7 +1052,7 @@ def createRequirementsJSON(cls, jPath=Path.cwd()):
1053
1052
indent = 4 ,
1054
1053
)
1055
1054
else :
1056
- jsonStep = json .dumps (
1055
+ json_step = json .dumps (
1057
1056
[
1058
1057
{
1059
1058
"step" : "install " + package ,
@@ -1062,96 +1061,101 @@ def createRequirementsJSON(cls, jPath=Path.cwd()):
1062
1061
],
1063
1062
indent = 4 ,
1064
1063
)
1065
- file .write (jsonStep )
1064
+ file .write (json_step )
1066
1065
1067
- def getLocalPackageVersion (packageList ):
1068
- '''Get package versions from the local environment. If the package
1066
+ @classmethod
1067
+ def get_local_package_version (cls , package_list ):
1068
+ """
1069
+ Get package_name versions from the local environment. If the package_name
1069
1070
does not contain an attribute of "__version__", "version", or
1070
- "VERSION", no package version will be found.
1071
+ "VERSION", no package_name version will be found.
1071
1072
1072
1073
Parameters
1073
1074
----------
1074
- packageList : list
1075
+ package_list : list
1075
1076
List of Python packages.
1076
1077
1077
1078
Returns
1078
1079
-------
1079
1080
list
1080
- Nested list of Python package names and found versions.
1081
- '''
1082
- def packageNotFoundOutput (package , packageAndVersion ):
1083
- print ("Warning: Package {} was not found in the local environment, so a version could not be determined." .format (package ))
1084
- print ("The pip installation command will not include a version number for {}." .format (package ))
1085
- packageAndVersion .append ([package , None ])
1086
- return packageAndVersion
1087
-
1088
- packageAndVersion = []
1081
+ Nested list of Python package_name names and found versions.
1082
+ """
1083
+ def package_not_found_output (package_name , package_versions ):
1084
+ print (f"Warning: Package { package_name } was not found in the local environment, so a version could not be "
1085
+ "determined." )
1086
+ print (f"The pip installation command will not include a version number for { package_name } ." )
1087
+ package_versions .append ([package_name , None ])
1088
+ return package_versions
1089
+
1090
+ package_and_version = []
1089
1091
import importlib
1090
- for package in packageList :
1092
+ for package in package_list :
1091
1093
try :
1092
1094
name = importlib .import_module (package )
1093
1095
try :
1094
- packageAndVersion .append ([package , name .__version__ ])
1096
+ package_and_version .append ([package , name .__version__ ])
1095
1097
except AttributeError :
1096
- pass
1097
1098
try :
1098
- packageAndVersion .append ([package , name .version ])
1099
+ package_and_version .append ([package , name .version ])
1099
1100
except AttributeError :
1100
1101
try :
1101
- packageAndVersion .append ([package , name .VERSION ])
1102
+ package_and_version .append ([package , name .VERSION ])
1102
1103
except AttributeError :
1103
- packageAndVersion = packageNotFoundOutput (package , packageAndVersion )
1104
+ package_and_version = package_not_found_output (package , package_and_version )
1104
1105
except ModuleNotFoundError :
1105
- packageAndVersion = packageNotFoundOutput (package , packageAndVersion )
1106
+ package_and_version = package_not_found_output (package , package_and_version )
1106
1107
1107
- return packageAndVersion
1108
+ return package_and_version
1108
1109
1109
- def getCodeDependencies (cls , jPath = Path .cwd ()):
1110
- '''Get the package dependencies for all Python scripts in the
1110
+ @classmethod
1111
+ def get_code_dependencies (cls , json_path = Path .cwd ()):
1112
+ """
1113
+ Get the package dependencies for all Python scripts in the
1111
1114
provided directory path. Note that currently this functionality
1112
1115
only works for .py files.
1113
1116
1114
1117
Parameters
1115
1118
----------
1116
- jPath : string, optional
1119
+ json_path : string, optional
1117
1120
File location for the output JSON file. Default is the current
1118
1121
working directory.
1119
1122
1120
1123
Returns
1121
1124
-------
1122
1125
list
1123
1126
List of found package dependencies.
1124
- '''
1125
- fileNames = []
1126
- fileNames .extend (sorted (Path (jPath ).glob ("*.py" )))
1127
-
1128
- importInfo = []
1129
- for file in fileNames :
1130
- importInfo .append (cls .findImports (file ))
1131
- importInfo = list (set (flatten (importInfo )))
1132
- return importInfo
1133
-
1134
- def findImports (fPath ):
1135
- '''Find import calls in provided Python code path. Ignores
1127
+ """
1128
+ file_names = []
1129
+ file_names .extend (sorted (Path (json_path ).glob ("*.py" )))
1130
+
1131
+ import_info = []
1132
+ for file in file_names :
1133
+ import_info .append (cls .find_imports (file ))
1134
+ import_info = list (set (flatten (import_info )))
1135
+ return import_info
1136
+
1137
+ @classmethod
1138
+ def find_imports (cls , file_path ):
1139
+ """
1140
+ Find import calls in provided Python code path. Ignores
1136
1141
built in Python modules.
1137
1142
1138
1143
Credit: modified from https://stackoverflow.com/questions/44988487/regex-to-parse-import-statements-in-python
1139
1144
1140
1145
Parameters
1141
1146
----------
1142
- fPath : string
1147
+ file_path : string or Path
1143
1148
File location for the Python file to be parsed.
1144
-
1145
1149
Returns
1146
1150
-------
1147
1151
list
1148
1152
List of found package dependencies.
1149
- '''
1153
+ """
1150
1154
import ast
1151
1155
1152
- fileText = open (fPath ).read ()
1156
+ file_text = open (file_path ).read ()
1153
1157
# Parse the file to get the abstract syntax tree representation
1154
- tree = ast .parse (fileText )
1158
+ tree = ast .parse (file_text )
1155
1159
modules = []
1156
1160
1157
1161
# Walk through each node in the ast to find import calls
@@ -1174,27 +1178,27 @@ def findImports(fPath):
1174
1178
except ValueError :
1175
1179
return modules
1176
1180
1177
- def getPickleFile (pPath = Path .cwd ()):
1181
+ @classmethod
1182
+ def get_pickle_file (cls , pickle_folder = Path .cwd ()):
1178
1183
"""
1179
1184
Given a file path, retrieve the pickle file(s).
1180
1185
1181
1186
Parameters
1182
1187
----------
1183
- pPath : str
1184
- File location for the input pickle file. Default is the current
1185
- working directory.
1186
-
1188
+ pickle_folder : str
1189
+ File location for the input pickle file. Default is the current working directory.
1187
1190
Returns
1188
1191
-------
1189
1192
list
1190
1193
A list of pickle files.
1191
1194
"""
1192
1195
1193
- fileNames = []
1194
- fileNames .extend (sorted (Path (pPath ).glob ("*.pickle" )))
1195
- return fileNames
1196
+ file_names = []
1197
+ file_names .extend (sorted (Path (pickle_folder ).glob ("*.pickle" )))
1198
+ return file_names
1196
1199
1197
- def getDependenciesFromPickleFile (cls , pickleFile ):
1200
+ @classmethod
1201
+ def get_pickle_dependencies (cls , pickle_file ):
1198
1202
"""
1199
1203
Reads the pickled byte stream from a file object, serializes the pickled byte
1200
1204
stream as a bytes object, and inspects the bytes object for all Python modules
@@ -1212,15 +1216,16 @@ def getDependenciesFromPickleFile(cls, pickleFile):
1212
1216
Python built-in modules are removed.
1213
1217
"""
1214
1218
1215
- with (open (pickleFile , "rb" )) as openfile :
1216
- obj = pickle .load (openfile )
1219
+ with (open (pickle_file , "rb" )) as open_file :
1220
+ obj = pickle .load (open_file )
1217
1221
dumps = pickle .dumps (obj )
1218
1222
1219
- modules = {mod .split ("." )[0 ] for mod , _ in cls .getPackageNames (dumps )}
1223
+ modules = {mod .split ("." )[0 ] for mod , _ in cls .get_package_names (dumps )}
1220
1224
modules .discard ("builtins" )
1221
1225
return list (modules )
1222
1226
1223
- def getPackageNames (stream ):
1227
+ @classmethod
1228
+ def get_package_names (cls , stream ):
1224
1229
"""
1225
1230
Generates (module, class_name) tuples from a pickle stream. Extracts all class names referenced
1226
1231
by GLOBAL and STACK_GLOBAL opcodes.
@@ -1239,62 +1244,62 @@ def getPackageNames(stream):
1239
1244
Generated (module, class_name) tuples.
1240
1245
"""
1241
1246
1242
- stack , markstack , memo = [], [], []
1247
+ stack , mark_stack , memo = [], [], []
1243
1248
mark = pickletools .markobject
1244
1249
1245
1250
# Step through the pickle stack and retrieve names used by STACK_GLOBAL
1246
1251
for opcode , arg , pos in pickletools .genops (stream ):
1247
1252
1248
1253
before , after = opcode .stack_before , opcode .stack_after
1249
- numtopop = len (before )
1254
+ number_to_pop = len (before )
1250
1255
1251
1256
if opcode .name == "GLOBAL" :
1252
1257
yield tuple (arg .split (1 , None ))
1253
1258
elif opcode .name == "STACK_GLOBAL" :
1254
- yield ( stack [- 2 ], stack [- 1 ])
1259
+ yield stack [- 2 ], stack [- 1 ]
1255
1260
elif mark in before or (opcode .name == "POP" and stack and stack [- 1 ] is mark ):
1256
- markpos = markstack .pop ()
1261
+ mark_stack .pop ()
1257
1262
while stack [- 1 ] is not mark :
1258
1263
stack .pop ()
1259
1264
stack .pop ()
1260
1265
try :
1261
- numtopop = before .index (mark )
1266
+ number_to_pop = before .index (mark )
1262
1267
except ValueError :
1263
- numtopop = 0
1268
+ number_to_pop = 0
1264
1269
elif opcode .name in {"PUT" , "BINPUT" , "LONG_BINPUT" , "MEMOIZE" }:
1265
1270
if opcode .name == "MEMOIZE" :
1266
1271
memo .append (stack [- 1 ])
1267
1272
else :
1268
1273
memo [arg ] = stack [- 1 ]
1269
- numtopop , after = 0 , [] # memoize and put; do not pop the stack
1274
+ number_to_pop , after = 0 , [] # memoize and put; do not pop the stack
1270
1275
elif opcode .name in {"GET" , "BINGET" , "LONG_BINGET" }:
1271
1276
arg = memo [arg ]
1272
1277
1273
- if numtopop :
1274
- del stack [- numtopop :]
1278
+ if number_to_pop :
1279
+ del stack [- number_to_pop :]
1275
1280
if mark in after :
1276
- markstack .append (pos )
1281
+ mark_stack .append (pos )
1277
1282
1278
1283
if len (after ) == 1 and opcode .arg is not None :
1279
1284
stack .append (arg )
1280
1285
else :
1281
1286
stack .extend (after )
1282
1287
1283
- def removeStdlibPackages (packageList ):
1284
- '''Remove any packages from the required list of installed packages that are part of the Python
1285
- Standard Library.
1288
+ @classmethod
1289
+ def remove_standard_library_packages (cls , package_list ):
1290
+ """
1291
+ Remove any packages from the required list of installed packages that are part of the Python Standard Library.
1286
1292
1287
1293
Parameters
1288
1294
----------
1289
- packageList : list
1295
+ package_list : list
1290
1296
List of all packages found that are not Python built-in packages.
1291
1297
1292
1298
Returns
1293
1299
-------
1294
1300
list
1295
- List of all packages found that are not Python built-in packages or part of the Python
1296
- Standard Library.
1297
- '''
1301
+ List of all packages found that are not Python built-in packages or part of the Python Standard Library.
1302
+ """
1298
1303
py10stdlib = ['_aix_support' , '_heapq' , 'lzma' , 'gc' , 'mailcap' , 'winsound' , 'sre_constants' , 'netrc' , 'audioop' ,
1299
1304
'xdrlib' , 'code' , '_pyio' , '_gdbm' , 'unicodedata' , 'pwd' , 'xml' , '_symtable' , 'pkgutil' , '_decimal' ,
1300
1305
'_compat_pickle' , '_frozen_importlib_external' , '_signal' , 'fcntl' , 'wsgiref' , 'uu' , 'textwrap' ,
@@ -1330,5 +1335,5 @@ def removeStdlibPackages(packageList):
1330
1335
'_curses_panel' , 'wave' , 'mmap' , 'warnings' , 'functools' , 'ipaddress' , 'nturl2path' , 'optparse' , '_queue' ,
1331
1336
'turtle' , 'spwd' , 'stat' , 'configparser' , '_warnings' , 'bdb' , '_osx_support' , 'typing' , 'zipfile' , 'glob' ,
1332
1337
'random' , 'smtplib' , 'plistlib' , 'hashlib' , '_struct' ]
1333
- packageList = [package for package in packageList if package not in py10stdlib ]
1334
- return packageList
1338
+ package_list = [package for package in package_list if package not in py10stdlib ]
1339
+ return package_list
0 commit comments