diff --git a/CHANGELOG.md b/CHANGELOG.md index 48bf3be68..955151352 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - #1024: Added flag -export-python-deps to publish command +- #1025: implement python dependency lifecycle and reference counting ### Fixed - #996: Ensure COS commands execute in exec under a dedicated, isolated context diff --git a/src/cls/IPM/Lifecycle/Base.cls b/src/cls/IPM/Lifecycle/Base.cls index 554a09815..6cca2df7e 100644 --- a/src/cls/IPM/Lifecycle/Base.cls +++ b/src/cls/IPM/Lifecycle/Base.cls @@ -491,6 +491,10 @@ Method %Clean(ByRef pParams) As %Status if $$$ISERR(tSC) { quit } + // validates the python libaraies need to be uninstalled as part of the clean process + if $get(pParams("with-py")) { + do ..UninstallPythonDeps(.pParams) + } } } catch e { set tSC = e.AsStatus() @@ -498,6 +502,26 @@ Method %Clean(ByRef pParams) As %Status quit tSC } +Method UninstallPythonDeps(pParams) +{ + if '##class(%IPM.Utils.Python).IsPythonEnabled() { + quit $$$OK + } + if '..Module.PythonDependencies.Count() { + quit $$$OK + } + do ..Log("Evaluating Python dependencies for removal...") + set key = "" + set ^||%IPM.PipCaller = ##class(%IPM.Utils.Python).ResolvePipCaller(.pParams) + for { + set pyRefOref = ..Module.PythonDependencies.GetNext(.key) + quit:key="" + set sc = ##class(%IPM.Storage.PythonReference).RemovePythonReference(key, .pParams) + $$$ThrowOnError(sc) + } + //kill ^||%IPM.PipCaller +} + Method %ExportData(ByRef pParams) As %Status { quit $$$OK @@ -746,16 +770,10 @@ Method InstallOrDownloadPythonRequirements( } set tSC = $$$OK try { - set target = ##class(%File).NormalizeDirectory("python", $system.Util.ManagerDirectory()) - if '$system.CLS.IsMthd("%SYS.Python", "Import") { - throw ##class(%Exception.General).%New("Embedded Python is not available in this instance.") - } set processType = "" - set tSysModule = ##class(%SYS.Python).Import("sys") - set tPyMajor = tSysModule."version_info".major - set tPyMinor = tSysModule."version_info".minor - set tPyMicro = tSysModule."version_info".micro - set tPyVersion = tPyMajor_"."_tPyMinor_"."_tPyMicro + set stdout = "" + set target = ##class(%IPM.Utils.Python).PythonManagerDir() + $$$ThrowOnError(##class(%IPM.Utils.Python).GetPythonVersion(.tPyVersion)) if download { set processType = "Download Python wheels" do ..Log(processType _ " START") @@ -783,6 +801,8 @@ Method InstallOrDownloadPythonRequirements( } set tSC = ##class(%IPM.Utils.Module).RunCommand(pRoot, command,.stdout) $$$ThrowOnError(tSC) + set tSC = ..SavePythonDependencies(pythonRequirements) + $$$ThrowOnError(tSC) } do ..Log(processType _ " SUCCESS") } catch ex { @@ -792,6 +812,41 @@ Method InstallOrDownloadPythonRequirements( quit tSC } +Method SavePythonDependencies(pythonFileLocation As %String = "") As %Status +{ + quit:pythonFileLocation="" $$$OK + do ..Log("Saving python dependencies from "_pythonFileLocation) + set file = ##class(%File).%New(pythonFileLocation) + set sc = file.Open("R") + if $$$ISERR(sc) { + return sc + } + while 'file.AtEnd { + set lib = file.ReadLine() + set lib = $zstrip(lib, "<>W") + if lib="" continue + if (lib [ ".whl") || (lib [ ".wheel") { + set whlFile = $piece($translate(lib, "\", "/"), "/", *) + set libraryName = $piece(whlFile, "-") + } else { + set libraryName = $piece(lib,"==") + } + // check does this Module already have this EXACT library and version? + set pyRefObj =..Module.PythonDependencies.GetAt($$$lcase(libraryName)) + if $isobject(pyRefObj) { + // already have this exact library and version, skip to next + if pyRefObj.IsVersionMatch(lib) { + continue + } + } + set pytonlibObj = ##class(%IPM.Storage.PythonReference).SavePythonDependencies(lib) + set sc = ..Module.PythonDependencies.SetAt(pytonlibObj, pytonlibObj.LibraryName) + } + // Save the python dependencies to the module object before reload. + set sc = ..Module.%Save() + return sc +} + ClassMethod ResolvePipCaller(ByRef pParams) As %List { set tUseStandalonePip = ##class(%IPM.Repo.UniversalSettings).GetValue("UseStandalonePip") diff --git a/src/cls/IPM/Main.cls b/src/cls/IPM/Main.cls index 5fce52dfa..11ba3e5ca 100644 --- a/src/cls/IPM/Main.cls +++ b/src/cls/IPM/Main.cls @@ -470,6 +470,13 @@ reinstall -env /path/to/env1.json;/path/to/env2.json example-package uninstall HS.JSON + + uninstall HS.JSON -with-py + + + + uninstall HS.JSON -r -with-py + @@ -481,6 +488,7 @@ reinstall -env /path/to/env1.json;/path/to/env2.json example-package + @@ -972,6 +980,7 @@ ClassMethod ShellInternal( set $$$ZPMCommandToLog = tCommand if (tCommandInfo = "quit") { + do ..OnBeforeShellExit() return } elseif (tCommandInfo = "help") { do ..%Help(.tCommandInfo) @@ -4075,6 +4084,15 @@ ClassMethod GetPythonInstalledLibs(Output list) } } +/// Clean up any temporary data or PPG before exiting the shell +ClassMethod OnBeforeShellExit() [ Internal ] +{ + try{ + kill ^||%IPM.PipCaller + }catch ex{ + } +} + ClassMethod GetPythonLibrariesList(PythonPath As %String) As %String [ Language = python ] { import sys diff --git a/src/cls/IPM/Storage/Module.cls b/src/cls/IPM/Storage/Module.cls index cb5c16eca..15d0cb9b2 100644 --- a/src/cls/IPM/Storage/Module.cls +++ b/src/cls/IPM/Storage/Module.cls @@ -87,6 +87,8 @@ Property Deployed As %Boolean(XMLPROJECTION = "Element"); Property UpdatePackage As %Dictionary.Classname; +Property PythonDependencies As array Of %IPM.Storage.PythonReference(XMLPROJECTION = "NONE"); + /// Iterates through all of a module's update step classes and tries to either execute or seed those steps. /// seedSteps: (Default) 0 = Execute Steps | 1 = Seeds Steps Method HandleAllUpdateSteps( @@ -1923,6 +1925,11 @@ Storage Default UpdatePackage + +PythonDependencies +subnode +"PythonDependencies" + ^IPM.Storage.ModuleD ModuleDefaultData ^IPM.Storage.ModuleD diff --git a/src/cls/IPM/Storage/PythonReference.cls b/src/cls/IPM/Storage/PythonReference.cls new file mode 100644 index 000000000..7220a29c0 --- /dev/null +++ b/src/cls/IPM/Storage/PythonReference.cls @@ -0,0 +1,176 @@ +/// store the python libraries mentioned in the requirements.txt file +Class %IPM.Storage.PythonReference Extends (%Persistent, %XML.Adaptor, %IPM.Utils.ComparisonAdaptor) +{ + +Parameter DEFAULTGLOBAL = "^IPM.Storage.PythonRefs"; + +Property LibraryName As %String(MAXLEN = 255) [ Required ]; + +Property Version As %String; + +Property SourceType As %String(VALUELIST = ",pypi,whl") [ InitialExpression = "pypi" ]; + +/// The physical path to the .whl file on the server +Property WheelPath As %String(MAXLEN = 1000); + +/// The specific platform tag (e.g., 'manylinux_2_17_x86_64' or 'win_amd64') +Property PlatformTag As %String(MAXLEN = 100); + +Property RefCount As %Integer [ InitialExpression = 1 ]; + +Index LibIdx On LibraryName [ IdKey, Unique ]; + +ClassMethod SavePythonDependencies(pLibrarySpec As %String) As %IPM.Storage.PythonReference +{ + set libraryInfo = ..ParseLibrarySpec(pLibrarySpec) + if libraryInfo="" { + quit $$$ERROR("Invalid library specification: "_pLibrarySpec) + } + set libraryName = $listget(libraryInfo) + set libararyVersion = $listget(libraryInfo,2) + set pyRef = ##class(%IPM.Storage.PythonReference).%OpenId(libraryName) + if $isobject(pyRef) { + set pyRef.RefCount = pyRef.RefCount + 1 + } else { + set pyRef = ..%New() + set pyRef.LibraryName = libraryName + } + if libararyVersion'="" { + set pyRef.Version = libararyVersion + } + if $listget(libraryInfo,3)="pypi" { + set pyRef.SourceType = $listget(libraryInfo,3) + set pyRef.PlatformTag = "" + set pyRef.WheelPath = "" + } + else { + set pyRef.SourceType = $listget(libraryInfo,3) + set pyRef.PlatformTag = $listget(libraryInfo,4) + set pyRef.WheelPath = $listget(libraryInfo,5) + } + $$$ThrowOnError(pyRef.%Save()) + quit pyRef +} + +ClassMethod ParseLibrarySpec(pLibrarySpec As %String) As %String +{ + if pLibrarySpec="" { + quit "" + } + if (pLibrarySpec[".whl")||(pLibrarySpec[".wheel") { + set filename = $piece($translate(pLibrarySpec, "\", "/"), "/", *) + set libName = $$$lcase($piece(filename, "-", 1)) + set platformTag = $piece($piece(filename, "-", 5),".") + set version = $piece(filename, "-", 2) + quit $listbuild(libName, version,"whl",platformTag,pLibrarySpec) + } + else { + set filename = $$$lcase($piece(pLibrarySpec,"==")) + set version = $piece(pLibrarySpec,"==",2) + quit $listbuild(filename,version,"pypi") + } +} + +/// Checks if this specific instance's version matches the libraryInfo +Method IsVersionMatch(pLibrarySpec As %String) As %Boolean +{ + set libraryInfo = ..ParseLibrarySpec(pLibrarySpec) + set version = $listget(libraryInfo, 2) + if version = "" { + quit 1 + } + quit ..Version = version +} + +ClassMethod IsPythonDependencyInstalled(pLibrarySpec As %String) As %Boolean +{ + set libraryInfo = ..ParseLibrarySpec(pLibrarySpec) + set libraryName = $listget(libraryInfo) + set version = $listget(libraryInfo, 2) + set pyRef = ##class(%IPM.Storage.PythonReference).%OpenId(libraryName) + if '$isobject(pyRef) { + quit 0 + } + if version'=""&&(pyRef.Version'=version) { + quit 0 + } + quit $$$OK +} + +ClassMethod RemovePythonReference( + pLibrarySpec As %String, + ByRef pParams) As %Status +{ + set libraryInfo = ..ParseLibrarySpec(pLibrarySpec) + set libraryName = $listget(libraryInfo) + set pyRef = ##class(%IPM.Storage.PythonReference).%OpenId(libraryName) + if '$isobject(pyRef) { + quit $$$OK + } + if pyRef.RefCount<=1 { + do ..UninstallPythonLibrary(libraryName, .pParams) + $$$ThrowOnError(..%DeleteId(libraryName)) + } else { + set pyRef.RefCount = pyRef.RefCount - 1 + $$$ThrowOnError(pyRef.%Save()) + } + quit $$$OK +} + +ClassMethod UninstallPythonLibrary( + pLibraryName As %List, + ByRef pParams) +{ + set verbose = $get(pParams("Verbose")) + set target = ##class(%File).NormalizeDirectory("python", $system.Util.ManagerDirectory()) + if '$listvalid(pLibraryName) { + set pLibraryName = $listfromstring(pLibraryName) + } + if '$data(^||%IPM.PipCaller, pipCaller) { + set pipCaller = ##class(%IPM.Utils.Python).ResolvePipCaller(.pParams) + } + set command = pipCaller _ $listbuild("uninstall", "-y")_pLibraryName + if $$$isUNIX { + set command = command _ $listbuild("--break-system-packages") + } + if verbose { + write !, "Running " + zwrite command + } else { + set stdout = "" + } + set tSC = ##class(%IPM.Utils.Module).RunCommand(target, command,.stdout) + $$$ThrowOnError(tSC) +} + +Storage Default +{ + + +%%CLASSNAME + + +Version + + +SourceType + + +WheelPath + + +PlatformTag + + +RefCount + + +^IPM.Storage.PythonRefsD +PythonReferenceDefaultData +^IPM.Storage.PythonRefsD +^IPM.Storage.PythonRefsI +^IPM.Storage.PythonRefsS +%Storage.Persistent +} + +} diff --git a/src/cls/IPM/Utils/Python.cls b/src/cls/IPM/Utils/Python.cls new file mode 100644 index 000000000..12f6641f6 --- /dev/null +++ b/src/cls/IPM/Utils/Python.cls @@ -0,0 +1,46 @@ +Include %occInclude + +/// Provides a centralized set of utility methods for interacting with +/// the Embedded Python engine within the IPM (InterSystems Package Manager) framework.
+/// This class handles environment verification, version retrieval, and +/// common Python operations used during the module lifecycle (install, uninstall, etc.). +Class %IPM.Utils.Python +{ + +ClassMethod GetPythonVersion(Output pyVersion As %String) As %Status +{ + set pyVersion = "" + set sc = ..IsPythonEnabled() + if $$$ISERR(sc) { + quit sc + } + + try { + set tSysModule = ##class(%SYS.Python).Import("sys") + set vInfo = tSysModule."version_info" + set pyVersion = vInfo.major _ "." _ vInfo.minor _ "." _ vInfo.micro + } catch ex { + set sc = ex.AsStatus() + } + return sc +} + +ClassMethod IsPythonEnabled() As %Status +{ + if '$system.CLS.IsMthd("%SYS.Python", "Import") { + return $$$ERROR($$$PythonGeneralError, "Embedded Python is not available in this instance.") + } + return $$$OK +} + +ClassMethod ResolvePipCaller(pParams) [ CodeMode = expression ] +{ +##class(%IPM.Lifecycle.Base).ResolvePipCaller(.pParams) +} + +ClassMethod PythonManagerDir() [ CodeMode = expression ] +{ +##class(%File).NormalizeDirectory("python", $system.Util.ManagerDirectory()) +} + +} diff --git a/tests/integration_tests/Test/PM/Integration/PythonReferenceLifeCycle.cls b/tests/integration_tests/Test/PM/Integration/PythonReferenceLifeCycle.cls new file mode 100644 index 000000000..75ab02e23 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/PythonReferenceLifeCycle.cls @@ -0,0 +1,179 @@ +Class Test.PM.Integration.PythonReferenceLifeCycle Extends Test.PM.Integration.Base +{ + +Method TestDependencyRegistration() +{ + #define NormalizeDirectory(%dir) ##class(%File).NormalizeDirectory(%dir) + #Define NormalizeFilename(%file,%dir) ##class(%File).NormalizeFilename(%file,%dir) + + set testRoot = $$$NormalizeDirectory($get(^UnitTestRoot)) + set module1Dir = $$$NormalizeDirectory(##class(%File).GetDirectory(testRoot)_"/_data/pythonref-lifecycle/module1/") + set requiredLibsFile = $$$NormalizeFilename("requirements.txt",module1Dir) + + set file = ##class(%File).%New(requiredLibsFile) + set sc = file.Open("R") + $$$ThrowOnError(sc) + while 'file.AtEnd { + set lib = file.ReadLine() + quit:lib="" + set lib = $zstrip(lib, "<>W") + set pythonLibs(lib)="" + } + do file.Close() + + set status = ..RunCommand("load "_module1Dir) + do $$$AssertStatusOK(status,"Loaded demo-module1 module successfully. " _ module1Dir) + + do ..VerifyDependenciesInstalled("demo-module1", .pythonLibs, 1) + + set module2Dir = $$$NormalizeDirectory(##class(%File).GetDirectory(testRoot)_"/_data/pythonref-lifecycle/module2/") + set status = ..RunCommand("load "_module2Dir) + do $$$AssertStatusOK(status,"Loaded demo-module2 module successfully. " _ module2Dir) + + do ..VerifyDependenciesInstalled("demo-module2", .pythonLibs, 2) + + do ..UninstallWithModifier(.pythonLibs) + + do $$$LogMessage("Reinstall the module for no-op uninstall test") + + // reinstall the modules + set status = ..RunCommand("load "_module1Dir) + do $$$AssertStatusOK(status,"Loaded demo-module1 module successfully. " _ module2Dir) + set status = ..RunCommand("load "_module2Dir) + do $$$AssertStatusOK(status,"Loaded demo-module2 module successfully. " _ module2Dir) + + do $$$LogMessage("Verifying the python library 'requests' installed as pypi demo-module2") + + set pyRefOref = ##class(%IPM.Storage.PythonReference).%OpenId("requests") + if $isobject(pyRefOref) { + do $$$AssertTrue(pyRefOref.SourceType="pypi", "Python library 'requests' has expected SourceType of 'pypi' for demo-module2") + } else { + do $$$AssertNotTrue(1, "Python library 'requests' has not expected SourceType of 'pypi' for demo-module2") + } + + do $$$LogMessage("Load the demo-module3 with '/home/irisowner/zpm/wheels/requests-2.32.4-py3-none-any.whl' dependency in requriements.txt") + + set module3Dir = $$$NormalizeDirectory(##class(%File).GetDirectory(testRoot)_"/_data/pythonref-lifecycle/module3/") + set status = ..RunCommand("load "_module3Dir) + do $$$AssertStatusOK(status,"Loaded demo-module3 module successfully. " _ module3Dir) + + do $$$LogMessage("Verifying the whl dependency is overwritten for demo-module3") + + set pyRefOref = ##class(%IPM.Storage.PythonReference).%OpenId("requests") + if $isobject(pyRefOref) { + do $$$AssertTrue(pyRefOref.SourceType="whl", "Python library 'requests' has expected SourceType of 'whl' for demo-module3. whl path is "_pyRefOref.WheelPath) + } else { + do $$$AssertNotTrue(1, "Python library 'requests' has not expected SourceType of 'whl' for demo-module3") + } + + do ..NoOpUninstall(.pythonLibs) + + do ..UninstallPyLibsEntriesManually(.pythonLibs) +} + +Method VerifyDependenciesInstalled( + pModule As %String, + ByRef PythonLibs, + ExpectedRefCount As %Integer) +{ + set module = ##class(%IPM.Storage.Module).NameOpen(pModule) + if '$isobject(module) { + do $$$AssertNotTrue(0,"Module "_pModule_" is not installed.") + quit + } + do $$$AssertTrue(1,"Module "_pModule_" is installed.") + do $$$LogMessage("Verifying python dependencies for module "_pModule_"...") + set key="" + for { + set pyRefOref = module.PythonDependencies.GetNext(.key) + quit:key="" + set libName = $$$lcase($piece(key,"==")) + if $data(PythonLibs(libName)) { + do $$$AssertTrue(1, "Python library "_libName_" is expected to be installed for module "_pModule) + do $$$AssertTrue(pyRefOref.RefCount=ExpectedRefCount, "Python library "_libName_" has expected reference count of "_pyRefOref.RefCount_" for module "_pModule) + }else { + do $$$AssertNotTrue(0, "Python library "_libName_" is NOT expected to be installed for module "_pModule) + } + } +} + +Method UninstallWithModifier(ByRef PythonLibs) +{ + do $$$LogMessage("Verifying the reference count before uninstall the demo-module1 with -with-py modifier...") + + do ..VerifyPythonLibRefCount(.PythonLibs, 2) + do $$$LogMessage("uninstall the modules with modifier -with-py") + set status = ..RunCommand("uninstall demo-module1 -with-py") + do $$$AssertStatusOK(status,"Uninstalled demo-module1 successfully.") + + do ..VerifyPythonLibRefCount(.PythonLibs, 1) + set status = ..RunCommand("uninstall demo-module2 -with-py") + do $$$AssertStatusOK(status,"Uninstalled demo-module2 successfully.") + do ..VerifyPythonLibExist(.PythonLibs) +} + +Method VerifyPythonLibExist(ByRef PythonLibs) +{ + set pyLib="" + do $$$LogMessage("Verifying python libraries exist...") + for { + set pyLib = $order(PythonLibs(pyLib)) quit:pyLib="" + set libName = $$$lcase($piece(pyLib,"==")) + set pyRefOref = ##class(%IPM.Storage.PythonReference).%OpenId(libName) + do $$$AssertTrue('$isobject(pyRefOref), "Python library "_libName_" not exists in PythonReference storage.") + } + do $$$LogMessage("Verifying python libraries exist check completed.") +} + +Method NoOpUninstall(ByRef PythonLibs) +{ + do $$$LogMessage("executing NoOpUninstall: uninstall modules without -with-py modifier") + for module="demo-module1","demo-module2","demo-module3" { + do $$$LogMessage("uninstall "_module_" the modules without removing python libraries...") + set status = ..RunCommand("uninstall "_module) + do $$$AssertStatusOK(status,"Uninstalled "_module_" successfully.") + do ..VerifyDependenciesInstalled(module, .PythonLibs, 2) + } +} + +Method VerifyPythonLibRefCount( + ByRef PythonLibs, + ExpectedRefCount As %Integer) +{ + set pyLib="" + do $$$LogMessage("Verifying python library reference counts. The count should 2.") + for { + set pyLib = $order(PythonLibs(pyLib)) quit:pyLib="" + set libName = $$$lcase($piece(pyLib,"==")) + set pyRefOref = ##class(%IPM.Storage.PythonReference).%OpenId(libName) + do $$$AssertTrue(pyRefOref.RefCount=ExpectedRefCount, "Python library "_libName_" has expected reference count of "_pyRefOref.RefCount) + } +} + +Method UninstallPyLibsEntriesManually(ByRef PythonLibs) +{ + set pyLib="" + do $$$LogMessage("Uninstalling python libraries entries manually...") + try { + for { + set pyLib = $order(PythonLibs(pyLib)) + quit:pyLib="" + set libName = $$$lcase($piece(pyLib,"==")) + continue:'##class(%IPM.Storage.PythonReference).%ExistsId(libName) + do $$$LogMessage("uninstall the python library "_libName_" via pip...") + do ##class(%IPM.Storage.PythonReference).UninstallPythonLibrary(libName) + set status = ##class(%IPM.Storage.PythonReference).%DeleteId(libName) + do $$$AssertTrue(status,"Removed python library "_libName_" from PythonReference storage.") + } + } catch ex { + do $$$AssertNotTrue(1,"Exception occurred during uninstalling python libraries entries manually: "_ex.DisplayString()) + } + do $$$LogMessage("Uninstalling python libraries entries completed.") +} + +Method RunCommand(pCommand As %String) As %Status [ CodeMode = expression ] +{ +##class(%IPM.Main).Shell(pCommand) +} + +} diff --git a/tests/integration_tests/Test/PM/Integration/_data/pythonref-lifecycle/module1/module.xml b/tests/integration_tests/Test/PM/Integration/_data/pythonref-lifecycle/module1/module.xml new file mode 100644 index 000000000..49ab2178f --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/pythonref-lifecycle/module1/module.xml @@ -0,0 +1,22 @@ + + + + + demo-module1 + 1.0.0 + description + keywords + + your name + your organization + 2020 + MIT + notes + + module + + + src + + + \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/pythonref-lifecycle/module1/requirements.txt b/tests/integration_tests/Test/PM/Integration/_data/pythonref-lifecycle/module1/requirements.txt new file mode 100644 index 000000000..a9db76f91 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/pythonref-lifecycle/module1/requirements.txt @@ -0,0 +1,2 @@ +regex +six \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/pythonref-lifecycle/module2/module.xml b/tests/integration_tests/Test/PM/Integration/_data/pythonref-lifecycle/module2/module.xml new file mode 100644 index 000000000..9e5b06bdb --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/pythonref-lifecycle/module2/module.xml @@ -0,0 +1,22 @@ + + + + + demo-module2 + 1.0.0 + description + keywords + + your name + your organization + 2020 + MIT + notes + + module + + + src + + + \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/pythonref-lifecycle/module2/requirements.txt b/tests/integration_tests/Test/PM/Integration/_data/pythonref-lifecycle/module2/requirements.txt new file mode 100644 index 000000000..8973f79b6 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/pythonref-lifecycle/module2/requirements.txt @@ -0,0 +1,3 @@ +requests +regex +six \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/pythonref-lifecycle/module3/module.xml b/tests/integration_tests/Test/PM/Integration/_data/pythonref-lifecycle/module3/module.xml new file mode 100644 index 000000000..db3c81f51 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/pythonref-lifecycle/module3/module.xml @@ -0,0 +1,22 @@ + + + + + demo-module3 + 1.0.1 + description + keywords + + your name + your organization + 2020 + MIT + notes + + module + + + src + + + \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/pythonref-lifecycle/module3/requirements.txt b/tests/integration_tests/Test/PM/Integration/_data/pythonref-lifecycle/module3/requirements.txt new file mode 100644 index 000000000..0d10ed092 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/pythonref-lifecycle/module3/requirements.txt @@ -0,0 +1 @@ +/home/irisowner/zpm/wheels/requests-2.32.4-py3-none-any.whl \ No newline at end of file