Skip to content

Commit 72e7cd7

Browse files
committed
Minor bug fixes; nested list/set handling
1 parent 5a96a32 commit 72e7cd7

File tree

1 file changed

+112
-40
lines changed

1 file changed

+112
-40
lines changed

src/sasctl/pzmm/write_json_files.py

Lines changed: 112 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,27 @@
1313
from scipy.stats import kendalltau, gamma
1414
import pickle
1515
import pickletools
16+
from collections.abc import Iterable
17+
18+
def flatten(nestedList):
19+
'''Flatten a nested list. Controls for str values in list, such that the str
20+
values are not expanded into a list of single characters.
21+
22+
Parameters
23+
----------
24+
nestedList : list
25+
A nested list of strings.
26+
27+
Yields
28+
------
29+
list
30+
A flattened list of strings.
31+
'''
32+
for item in nestedList:
33+
if isinstance(item, Iterable) and not isinstance(item, (str, bytes)):
34+
yield from flatten(item)
35+
else:
36+
yield item
1637

1738
class JSONFiles:
1839
@classmethod
@@ -997,13 +1018,14 @@ def createRequirementsJSON(cls, jPath=Path.cwd()):
9971018
picklePackages = []
9981019
pickleFiles = cls.getPickleFile(jPath)
9991020
for pickleFile in pickleFiles:
1000-
picklePackages.append(cls.getDependenciesFromPickleFile(pickleFile))
1001-
1002-
codeDependencies = cls.getCodeDependencies(jPath)
1021+
picklePackages.append(cls.getDependenciesFromPickleFile(cls, pickleFile))
10031022

1004-
packageList = picklePackages + codeDependencies
1005-
packageAndVersion = cls.getLocalPackageVersion(list(set(packageList)))
1023+
codeDependencies = cls.getCodeDependencies(cls, jPath)
10061024

1025+
packageList = list(picklePackages) + codeDependencies
1026+
packageList = list(set(list(flatten(packageList))))
1027+
packageList = cls.removeStdlibPackages(packageList)
1028+
packageAndVersion = cls.getLocalPackageVersion(packageList)
10071029
# Identify packages with missing versions
10081030
missingPackageVersions = [item[0] for item in packageAndVersion if not item[1]]
10091031

@@ -1012,7 +1034,7 @@ def createRequirementsJSON(cls, jPath=Path.cwd()):
10121034
jsonStep = json.dumps(
10131035
[
10141036
{
1015-
"Warning": "The versions for the following packages could not be determined",
1037+
"Warning": "The versions for the following packages could not be determined:",
10161038
"Packages": ", ".join(missingPackageVersions)
10171039
}
10181040
],
@@ -1042,10 +1064,10 @@ def createRequirementsJSON(cls, jPath=Path.cwd()):
10421064
)
10431065
file.write(jsonStep)
10441066

1045-
def getLocalPackageVersion(self, packageList):
1046-
'''Get package versions from the local environment. For Python versions
1047-
< 3.8, if the package does not contain an attribute of "__version__",
1048-
"version", or "VERSION", no package version will be found.
1067+
def getLocalPackageVersion(packageList):
1068+
'''Get package versions from the local environment. If the package
1069+
does not contain an attribute of "__version__", "version", or
1070+
"VERSION", no package version will be found.
10491071
10501072
Parameters
10511073
----------
@@ -1057,20 +1079,16 @@ def getLocalPackageVersion(self, packageList):
10571079
list
10581080
Nested list of Python package names and found versions.
10591081
'''
1060-
packageAndVersion = []
1061-
if sys.version_info[1] >= 8:
1062-
from importlib.metadata import version
1063-
for package in packageList:
1064-
try:
1065-
packageAndVersion.append([package,version(package)])
1066-
except PackageNotFoundError:
1067-
print("Warning: Package {} was not found in the local environment, so a version could not be determined.".format(package))
1068-
print("The pip installation command will not include a version number for {}.").format(package)
1069-
packageAndVersion.append([package, None])
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])
10701086
return packageAndVersion
1071-
else:
1072-
import importlib
1073-
for package in packageList:
1087+
1088+
packageAndVersion = []
1089+
import importlib
1090+
for package in packageList:
1091+
try:
10741092
name = importlib.import_module(package)
10751093
try:
10761094
packageAndVersion.append([package, name.__version__])
@@ -1082,12 +1100,13 @@ def getLocalPackageVersion(self, packageList):
10821100
try:
10831101
packageAndVersion.append([package, name.VERSION])
10841102
except AttributeError:
1085-
print("Warning: Package {} was not found in the local environment, so a version could not be determined.".format(package))
1086-
print("The pip installation command will not include a version number for {}.".format(package))
1087-
packageAndVersion.append([package, None])
1088-
return packageAndVersion
1103+
packageAndVersion = packageNotFoundOutput(package, packageAndVersion)
1104+
except ModuleNotFoundError:
1105+
packageAndVersion = packageNotFoundOutput(package, packageAndVersion)
10891106

1090-
def getCodeDependencies(self, jPath=Path.cwd()):
1107+
return packageAndVersion
1108+
1109+
def getCodeDependencies(cls, jPath=Path.cwd()):
10911110
'''Get the package dependencies for all Python scripts in the
10921111
provided directory path. Note that currently this functionality
10931112
only works for .py files.
@@ -1108,12 +1127,11 @@ def getCodeDependencies(self, jPath=Path.cwd()):
11081127

11091128
importInfo = []
11101129
for file in fileNames:
1111-
importInfo.append(self.findImports(file))
1112-
importInfo = list(set(importInfo))
1113-
1130+
importInfo.append(cls.findImports(file))
1131+
importInfo = list(set(flatten(importInfo)))
11141132
return importInfo
11151133

1116-
def findImports(self, fPath):
1134+
def findImports(fPath):
11171135
'''Find import calls in provided Python code path. Ignores
11181136
built in Python modules.
11191137
@@ -1156,7 +1174,7 @@ def findImports(self, fPath):
11561174
except ValueError:
11571175
return modules
11581176

1159-
def getPickleFile(self, pPath):
1177+
def getPickleFile(pPath=Path.cwd()):
11601178
"""
11611179
Given a file path, retrieve the pickle file(s).
11621180
@@ -1176,7 +1194,7 @@ def getPickleFile(self, pPath):
11761194
fileNames.extend(sorted(Path(pPath).glob("*.pickle")))
11771195
return fileNames
11781196

1179-
def getDependenciesFromPickleFile(self, pickleFile):
1197+
def getDependenciesFromPickleFile(cls, pickleFile):
11801198
"""
11811199
Reads the pickled byte stream from a file object, serializes the pickled byte
11821200
stream as a bytes object, and inspects the bytes object for all Python modules
@@ -1189,19 +1207,20 @@ def getDependenciesFromPickleFile(self, pickleFile):
11891207
11901208
Returns
11911209
-------
1192-
set
1193-
A set of modules obtained from the pickle stream.
1210+
list
1211+
A list of modules obtained from the pickle stream. Duplicates are removed and
1212+
Python built-in modules are removed.
11941213
"""
11951214

11961215
with (open(pickleFile, "rb")) as openfile:
11971216
obj = pickle.load(openfile)
11981217
dumps = pickle.dumps(obj)
11991218

1200-
modules = {mod.split(".")[0] for mod, _ in self.getPackageNames(dumps)}
1219+
modules = {mod.split(".")[0] for mod, _ in cls.getPackageNames(dumps)}
12011220
modules.discard("builtins")
1202-
return modules
1221+
return list(modules)
12031222

1204-
def getPackageNames(self, stream):
1223+
def getPackageNames(stream):
12051224
"""
12061225
Generates (module, class_name) tuples from a pickle stream. Extracts all class names referenced
12071226
by GLOBAL and STACK_GLOBAL opcodes.
@@ -1259,4 +1278,57 @@ def getPackageNames(self, stream):
12591278
if len(after) == 1 and opcode.arg is not None:
12601279
stack.append(arg)
12611280
else:
1262-
stack.extend(after)
1281+
stack.extend(after)
1282+
1283+
def removeStdlibPackages(packageList):
1284+
'''Remove any packages from the required list of installed packages that are part of the Python
1285+
Standard Library.
1286+
1287+
Parameters
1288+
----------
1289+
packageList : list
1290+
List of all packages found that are not Python built-in packages.
1291+
1292+
Returns
1293+
-------
1294+
list
1295+
List of all packages found that are not Python built-in packages or part of the Python
1296+
Standard Library.
1297+
'''
1298+
py10stdlib = ['_aix_support', '_heapq', 'lzma', 'gc', 'mailcap', 'winsound', 'sre_constants', 'netrc', 'audioop',
1299+
'xdrlib', 'code', '_pyio', '_gdbm', 'unicodedata', 'pwd', 'xml', '_symtable', 'pkgutil', '_decimal',
1300+
'_compat_pickle', '_frozen_importlib_external', '_signal', 'fcntl', 'wsgiref', 'uu', 'textwrap',
1301+
'_codecs_iso2022', 'keyword', 'distutils', 'binascii', 'email', 'reprlib', 'cmd', 'cProfile',
1302+
'dataclasses', '_sha512', 'ntpath', 'readline', 'signal', '_elementtree', 'dis', 'rlcompleter',
1303+
'_json', '_ssl', '_sha3', '_winapi', 'telnetlib', 'pyexpat', '_lzma', 'http', 'poplib', 'tokenize',
1304+
'_dbm', '_io', 'linecache', 'json', 'faulthandler', 'hmac', 'aifc', '_csv', '_codecs_hk', 'selectors',
1305+
'_random', '_pickle', '_lsprof', 'turtledemo', 'cgitb', '_sitebuiltins', 'binhex', 'fnmatch',
1306+
'sysconfig', 'datetime', 'quopri', 'copyreg', '_pydecimal', 'pty', 'stringprep', 'bisect', '_abc',
1307+
'_codecs_jp', '_md5', 'errno', 'compileall', '_threading_local', 'dbm', 'builtins', 'difflib',
1308+
'imghdr', '__future__', '_statistics', 'getopt', 'xmlrpc', '_sqlite3', '_sha1', 'shelve',
1309+
'_posixshmem', 'struct', 'timeit', 'ensurepip', 'pathlib', 'ctypes', '_multiprocessing', 'tty',
1310+
'_weakrefset', 'sqlite3', 'tracemalloc', 'venv', 'unittest', '_blake2', 'mailbox', 'resource',
1311+
'shutil', 'winreg', '_opcode', '_codecs_tw', '_operator', 'imp', '_string', 'os', 'opcode',
1312+
'_zoneinfo', '_posixsubprocess', 'copy', 'symtable', 'itertools', 'sre_parse', '_bisect', '_imp', 're',
1313+
'ast', 'zlib', 'fractions', 'pickle', 'profile', 'sys', 'ssl', 'cgi', 'enum', 'modulefinder',
1314+
'py_compile', '_curses', '_functools', 'cmath', '_crypt', 'contextvars', 'math', 'uuid', 'argparse',
1315+
'_frozen_importlib', 'inspect', 'posix', 'statistics', 'marshal', 'nis', '_bz2', 'pipes',
1316+
'socketserver', 'pstats', 'site', 'trace', 'lib2to3', 'zipapp', 'runpy', 'sre_compile', 'time',
1317+
'pprint', 'base64', '_stat', '_ast', 'pdb', '_markupbase', '_bootsubprocess', '_collections', '_sre',
1318+
'msilib', 'crypt', 'gettext', 'mimetypes', '_overlapped', 'asyncore', 'zipimport', 'chunk', 'atexit',
1319+
'graphlib', '_multibytecodec', 'gzip', 'io', 'logging', 'nntplib', 'genericpath', 'syslog', 'token',
1320+
'_msi', 'idlelib', '_hashlib', 'threading', 'select', 'doctest', 'getpass', '_sha256', 'importlib',
1321+
'_tracemalloc', 'multiprocessing', 'calendar', '_codecs_cn', '_tkinter', '_uuid', 'socket',
1322+
'antigravity', 'string', '_locale', '_thread', 'grp', 'this', 'zoneinfo', 'abc', 'operator', 'colorsys',
1323+
'tabnanny', '_weakref', 'imaplib', 'concurrent', 'subprocess', '_compression', 'pyclbr', 'tarfile',
1324+
'numbers', 'queue', 'posixpath', 'smtpd', 'webbrowser', 'asynchat', 'weakref', 'filecmp', 'decimal',
1325+
'_py_abc', 'collections', 'tempfile', '_collections_abc', 'sched', 'locale', 'secrets', 'msvcrt',
1326+
'asyncio', 'array', '_codecs_kr', '_scproxy', '_strptime', 'heapq', '_socket', 'sndhdr', 'types', 'nt',
1327+
'_datetime', 'shlex', 'tkinter', 'curses', 'encodings', 'pickletools', 'html', '_codecs', 'codeop',
1328+
'_ctypes', 'bz2', 'contextlib', 'platform', 'termios', '_asyncio', 'ftplib', 'pydoc_data',
1329+
'_contextvars', 'codecs', 'traceback', 'pydoc', 'fileinput', 'ossaudiodev', 'urllib', 'csv', 'sunau',
1330+
'_curses_panel', 'wave', 'mmap', 'warnings', 'functools', 'ipaddress', 'nturl2path', 'optparse', '_queue',
1331+
'turtle', 'spwd', 'stat', 'configparser', '_warnings', 'bdb', '_osx_support', 'typing', 'zipfile', 'glob',
1332+
'random', 'smtplib', 'plistlib', 'hashlib', '_struct']
1333+
packageList = [package for package in packageList if package not in py10stdlib]
1334+
return packageList

0 commit comments

Comments
 (0)