Script for listing all dependencies

Pharanesch

New member
Messages
21
Reactions
7
Points
3
For a while I've been thinking of making a script that can list all dependencies in all vars and presets, or list which vars depend on a specific var. After getting inspired by le_hibous pyhton script (https://hub.virtamate.com/threads/too-many-heap-sections.38525/#post-108718), I finally got down to making it.

The main functionality is the following:
python "<Path_to_file>\dependencyScanner.py" -p "<VaM folder path>" [-n "<name_of_var>"] [-o ["<output_file>"]]
name_of_var can also be a partial name, the script will simply list every var that contains the part in its name.

A second function is that when you leave out -n "<name_of_var>", it will list every var in the folder that isn't depended on by any other var.
It is now also possible to save the results to a file, using the -o flag. If no name is given to the flag, it defaults to "SearchVarOutput.txt"
In case you have unzipped vars in the AddonPackages folder, it should catch those too.

I would like to make it into a plugin at some point, but that's a whole other matter.

Thoughts and feedback?

Example with -n ascorad

Code:
The following 7 vars are not used as a dependency in other vars:
        ascorad.Adela_Scene.1
        ascorad.Amia_Scene.1
        ascorad.Ariadne_-_Moving_Day.7
        ascorad.Enjoying_Evey.2
        ascorad.Haley&Tess_Heads_Will_Roll_v2.5
        ascorad.Haley_Armageddon_5.29
        ascorad.Katia.2

The following 8 iterations of 'ascorad' has other vars that depend on it:
ascorad.Amia.latest ->
        ascorad.Amia_Scene.1.var
ascorad.Ariadne_Look.latest ->
        ascorad.Ariadne_-_Moving_Day.7.var
ascorad.asco_Expressions.latest ->
        ascorad.Amia_Scene.1.var
        ascorad.Ariadne_-_Moving_Day.7.var
        ascorad.Enjoying_Evey.2.var
        ascorad.Katia_Scene.2.var
ascorad.ascorad_Haley_v5_2.latest ->
        coll69.Haley_At_Your_Service.12.var
ascorad.Breast_Morphs.latest ->
        VamTimbo.SexyDance_5-17-23.5.var
ascorad.Livie.latest ->
        ascorad.Vivie_&_Livie_Scene.4.var
ascorad.Pella.latest ->
        ascorad.Vivie_Scene.4.var
ascorad.Preset_Adela.latest ->
        ascorad.Adela_Scene.1.var

Python:
#!/usr/bin/python3
from zipfile import ZipFile
from glob import glob
from os.path import join, split, splitext, exists, basename, isdir, dirname
import sys
import argparse
from pathlib import Path
from json import load
import re

def dpPrint(text: str, toFile: str = ''):
    print(text, flush=True)
    if toFile:
        try:
            with open(toFile, 'a', encoding='utf-8') as f:
                f.write(text + '\n')
        except Exception as error:
            print(f"ERROR, failed to write to {toFile}: {error}", file=sys.stderr, flush=True)
    return

def check_name_variation(varName, dependenciesList):
    parts = varName.split('.')
    if parts[-1].isdigit() or parts[-1] == 'latest':
        baseName = '.'.join(parts[:-1])
    else:
        baseName = varName
    return varName in dependenciesList or f'{baseName}.latest' in dependenciesList

def getDependencies(varFile: str) -> tuple[Exception|None, set]:
    try:
        with ZipFile(varFile) as myzip:
            with myzip.open('meta.json') as metaJson:
                data = load(metaJson)
            myzip.close()
       
        if 'dependencies' not in data:
            return None, set()
        return None, set(data['dependencies'].keys())
    except Exception as error:
        return error, set()

# Get all dependencies listed in presets
def getPresetDependencies(customPath: str) -> dict:
    allDependencies = {}
    presets = sorted(glob(join(customPath, 'Custom', '**/*.vap'), recursive=True))
 
    for vap in presets:
        try:  
            with open(vap, 'r', encoding='utf-8') as f:
                pattern = r'"([^"]+):/[^"]*"'
                occurences = set(re.findall(pattern, f.read()))
        except Exception as error:
            dpPrint(f"ERROR, failed to read: {vap}: {error}", args.output)
            continue
   
        parts = Path(vap).parts
        presetName = '/'.join(parts[parts.index("Custom")+1:])
        dependenciesDict = {key: {presetName} for key in occurences}
        for key, val in dependenciesDict.items():
            if key in allDependencies:
                allDependencies[key].update(val)
            else:
                allDependencies[key] = val
    return allDependencies

# Collect all vars, a complete list of all dependencies, and which vars uses which dependency
def getAllVars(directoryPath: str) -> tuple[set, dict]:
    allDependencies = {}
 
    vars = glob(join(directoryPath, 'AddonPackages', '**/*.var'), recursive=True)
    allVarList = {splitext(basename(var))[0] for var in vars}
 
    for varFilename in vars:
        if isdir(varFilename):
            error, dependencies = getMetaDependencies(varFilename)
        else:
            error, dependencies = getDependencies(varFilename)
   
        if (isinstance(error, Exception)):
            dpPrint(f"ERROR: {varFilename}: {error}", args.output)
            continue
   
        if (len(dependencies) == 0):
            continue

        dependenciesDict = {key: {split(varFilename)[1]} for key in dependencies}
        for key, val in dependenciesDict.items():
            if key in allDependencies:
                allDependencies[key].update(val)
            else:
                allDependencies[key] = val
 
    return allVarList, allDependencies

# Get all dependencies listed in meta.json stored directly in folders
def getMetaDependencies(folder: str) -> tuple[Exception|None, set]:
    filename = join(folder, 'meta.json')
    try:
        with open(filename, 'r', encoding='utf-8') as metaJson:
            data = load(metaJson)
           
        if 'dependencies' not in data:
            return None, set()
        return None, set(data['dependencies'].keys())
    except Exception as error:
        return error, set()
   

argParser = argparse.ArgumentParser(prog='DependencyScanner.py', description='Searches for dependencies inside all vars in a folder and if a name is given, lists whether there a vars that depend on it. If no name is given, it will instead list all vars in the folder which isn\'t a dependency of any other var.')
argParser.add_argument("-p", "--path",
                        type=Path,
                        default='.',
                        help="Path your VaM folder")
argParser.add_argument("-n", "--name",
                        type=str,
                        default='',
                        help="Name of a var file. Can be a partial name")
argParser.add_argument("-o", "--output",
                        type=Path,
                        nargs='?',
                        default=None,
                        const='.\\SearchVarOutput.txt',
                        help="File to save the results in (default: '.\\SearchVarOutput.txt')")

args = argParser.parse_args()

# Check the given arguments are valid
if not exists(args.path):
    print(f"The given path doesn't exist: {args.path}")
    exit(1)

if args.output:
    if isdir(args.output):
        print(f"The given output is a folder, not a file: {args.output}")
        exit(1)

    if not isdir(dirname(args.output)) and not basename(args.output) != '':
        print(f"The given output file folder path doesn't exist: {args.output}")
        exit(1)

    open(args.output, 'w', encoding='utf-8').close()

parts = args.name.split('.')
if parts[-1] == 'var':
    args.name = '.'.join(parts[:-1])

# Get a list of all vars and a dict of all dependencies from vars
allVarList, allVarDependencies = getAllVars(args.path)
allDepVars = list(allVarDependencies.keys())

# Get a dict of all dependencies from presets
allPresetDependencies = getPresetDependencies(args.path)
allDepVarsPresets = list(allPresetDependencies.keys())

# Combine all dependencies
allDependencies = allVarDependencies | allPresetDependencies

# Get a list of which vars in the folder that are not used as a dependency
noDependencyVar = [var for var in allVarList if not check_name_variation(var, allDepVars)]
noDependencyPreset = [var for var in allVarList if not check_name_variation(var, allDepVarsPresets)]
noDependency = sorted(list(set(noDependencyVar) & set(noDependencyPreset)), key=str.lower)

# If no name is given, list all vars that aren't used as a dependency
if args.name == '':
    dpPrint(f"{len(noDependency)} vars are not used as a dependency:", args.output)
    for var in noDependency:
        dpPrint("\t" + var, args.output)
    if args.output:
        print(f"\nResults saved to: {args.output}")
    exit(0)

# List all vars with the given name that aren't used as a dependency
foundVars = [var for var in noDependency if args.name.lower() in var.lower()]
if len(foundVars) > 0:
    dpPrint(f"The following {str(len(foundVars)) + ' vars are' if len(foundVars) > 1 else 'var is'} not used as a dependency in other vars:", args.output)
    for var in foundVars:
        dpPrint("\t" + var, args.output)

# List all vars with the given name that depend on other vars
foundDepVars = sorted([var for var in allDependencies.keys() if args.name.lower() in var.lower()], key=str.lower)
if len(foundDepVars) > 0:
    dpPrint(f"\nThe following {len(foundDepVars)} iteration{'s' if len(foundDepVars) > 1 else ''} of '" + args.name + "' has other vars that depend on it:", args.output)

    # print all dependencies where the name is a key, or the name is part of the key
    for key in foundDepVars:
        if args.name.lower() in key.lower():
            if args.name != key:
                dpPrint(f"{key} ->", args.output)
            for var in sorted(allDependencies[key], key=str.lower):
                dpPrint("\t" + var, args.output)

if args.output:
    print(f"\nResults saved to: {args.output}")

Edit: added the option of saving the result to a file
Edit2: should now also catch packages stored as folders with meta.json inside them
 
Last edited:
I'm not getting any results. I know none of my vars are used as dependencies in other vars, so it should be listing those.

python "List_Deps.py" -p "C:\Games\VaM\AddonPackages"
The following 0 vars are not used as a dependency:
 
I'm not getting any results. I know none of my vars are used as dependencies in other vars, so it should be listing those.

python "List_Deps.py" -p "C:\Games\VaM\AddonPackages"
The following 0 vars are not used as a dependency:
Just write the following instead.
python "List_Deps.py" -p "C:\Games\VaM"
The program targets the vam root folder.
 
Honestly, I think that as a script is better than making it as a VaM plugin. If its purpose is file checking you'll likely not be on VaM anyway to delete the VARs, so the script option is simpler and more flexible.

A while back I made one to check on presets. Maybe something in there could be useful to expand yours or give ideas:

 
Honestly, I think that as a script is better than making it as a VaM plugin. If its purpose is file checking you'll likely not be on VaM anyway to delete the VARs, so the script option is simpler and more flexible.

A while back I made one to check on presets. Maybe something in there could be useful to expand yours or give ideas:

My idea for making it a plugin, was that you could then get the thumbnails of the found vars. But I agree, the script option is indeed easier to work with. Plus I know absolutely nothing about making plugins. But could be a fun exercise in the future.
 
Just write the following instead.
python "List_Deps.py" -p "C:\Games\VaM"
The program targets the vam root folder.
OK, now the report is so long, I need to redirect to a file. :ROFLMAO: Thanks!

Here's a legit request for an enhancement. Some of the .vars I have have been uncompressed into folders for various reasons. The script fails to examine these, giving a Permission denied error, because they can't be opened with a zip tool.

I think it also needs an option to redirect all output, normal and errors to a file. I tried the usual > operator, but that only got the non-error output. The 2>&1 construct didn't work.
 
OK, now the report is so long, I need to redirect to a file. :ROFLMAO: Thanks!

Here's a legit request for an enhancement. Some of the .vars I have have been uncompressed into folders for various reasons. The script fails to examine these, giving a Permission denied error, because they can't be opened with a zip tool.

I think it also needs an option to redirect all output, normal and errors to a file. I tried the usual > operator, but that only got the non-error output. The 2>&1 construct didn't work.
When you say uncompressed, do you mean that the var has been unzipped to a folder? If that is the case, I could probably just look for any meta.json in the folder.

It should be quite simple to add an option to write to a file. Will probably just be another flag with a folderpath as input.
 
When you say uncompressed, do you mean that the var has been unzipped to a folder? If that is the case, I could probably just look for any meta.json in the folder.

It should be quite simple to add an option to write to a file. Will probably just be another flag with a folderpath as input.
Correct. A while back, if there were things I didn't like in a var, I would unzip it to a directory, move the var file out and rename the folder to have the original var's name. Then I could edit things in the original scene I didn't like, such as dependencies I really didn't want. Vam accepts this.
 
Correct. A while back, if there were things I didn't like in a var, I would unzip it to a directory, move the var file out and rename the folder to have the original var's name. Then I could edit things in the original scene I didn't like, such as dependencies I really didn't want. Vam accepts this.
I can probably just look for any meta.json files in any of the folders then. Then it's just a question of extracting the packagename and creator from it.

Btw, I've updated the script with an -o flag that can be used to output the results to a file.
 
Correct. A while back, if there were things I didn't like in a var, I would unzip it to a directory, move the var file out and rename the folder to have the original var's name. Then I could edit things in the original scene I didn't like, such as dependencies I really didn't want. Vam accepts this.
I think it should work with folder version of packages now.
 
Sorry, no. The second error below is due to some absurdly long description field in the meta.json. I think there are some non-ASCII characters in there. It doesn't like something in the text of the description. The Permission denied errors are folders.

ERROR: C:\Games\VaM\AddonPackages\A1X.LUCY_CYBER.1.var: [Errno 13] Permission denied: 'C:\\Games\\VaM\\AddonPackages\\A1X.LUCY_CYBER.1.var'
ERROR: C:\Games\VaM\AddonPackages\Amaimon.Bhaviour.2.var: Expecting ',' delimiter: line 7 column 1076 (char 1266)
ERROR: C:\Games\VaM\AddonPackages\Anonymous Chunk.Soph_L_&_ALT_V1.1.var: [Errno 13] Permission denied: 'C:\\Games\\VaM\\AddonPackages\\Anonymous Chunk.Soph_L_&_ALT_V1.1.var'
ERROR: C:\Games\VaM\AddonPackages\Anonymous Chunk.Soph_L_&_ALT_V1_2.1.var: [Errno 13] Permission denied: 'C:\\Games\\VaM\\AddonPackages\\Anonymous Chunk.Soph_L_&_ALT_V1_2.1.var'
...
 
Sorry, no. The second error below is due to some absurdly long description field in the meta.json. I think there are some non-ASCII characters in there. It doesn't like something in the text of the description. The Permission denied errors are folders.

ERROR: C:\Games\VaM\AddonPackages\A1X.LUCY_CYBER.1.var: [Errno 13] Permission denied: 'C:\\Games\\VaM\\AddonPackages\\A1X.LUCY_CYBER.1.var'
ERROR: C:\Games\VaM\AddonPackages\Amaimon.Bhaviour.2.var: Expecting ',' delimiter: line 7 column 1076 (char 1266)
ERROR: C:\Games\VaM\AddonPackages\Anonymous Chunk.Soph_L_&_ALT_V1.1.var: [Errno 13] Permission denied: 'C:\\Games\\VaM\\AddonPackages\\Anonymous Chunk.Soph_L_&_ALT_V1.1.var'
ERROR: C:\Games\VaM\AddonPackages\Anonymous Chunk.Soph_L_&_ALT_V1_2.1.var: [Errno 13] Permission denied: 'C:\\Games\\VaM\\AddonPackages\\Anonymous Chunk.Soph_L_&_ALT_V1_2.1.var'
...
I misunderstood how you named the folder. Didn't actually know you could do it like that.
What went wrong was that the code simply thought it was a var file instead of a folder and then tried to unzip it. It should now check for it and do the right thing.

With the non-ascii character, I've tried solving it by using utf-8 encodin when opening, but it might not solve it completely. I ran into some cases where the json file was simply wrongly formatted, and could thus not be read by the json.load() function. Not entirely sure how to work around that without whipping out the big regex machine.

May I ask which var it is? I would like to check the meta.json in it.
 
I misunderstood how you named the folder. Didn't actually know you could do it like that.
What went wrong was that the code simply thought it was a var file instead of a folder and then tried to unzip it. It should now check for it and do the right thing.

With the non-ascii character, I've tried solving it by using utf-8 encodin when opening, but it might not solve it completely. I ran into some cases where the json file was simply wrongly formatted, and could thus not be read by the json.load() function. Not entirely sure how to work around that without whipping out the big regex machine.

May I ask which var it is? I would like to check the meta.json in it.
It's the Amaimon.Bhaviour.2.var in the message. It's only a dependency for something else.
 
The unzipped var folders do work now, thanks. It looks like this thing is very good at sniffing out vars that are not formatted correctly inside. These errors are all legit:

ERROR: C:\Games\VaM\AddonPackages\Amaimon.Bhaviour.2.var: Expecting ',' delimiter: line 7 column 1076 (char 1266)
ERROR: C:\Games\VaM\AddonPackages\AshAuryn.AshAuryn_HappySurprise_Expression_Morph.4.var: "There is no item named 'meta.json' in the archive"
ERROR: C:\Games\VaM\AddonPackages\AshAuryn.MacGruber_Life13_Modified_Breathing_Driver.6.var: Expecting ',' delimiter: line 26 column 5 (char 935)
ERROR: C:\Games\VaM\AddonPackages\AshAuryn.MacGruber_Life13_Modified_Breathing_Driver.7.var: Expecting ',' delimiter: line 31 column 4 (char 1355)
ERROR: C:\Games\VaM\AddonPackages\Frief.vikky.1.var: Expecting property name enclosed in double quotes: line 215 column 4 (char 7325)
ERROR: C:\Games\VaM\AddonPackages\snzhkhd.Leigh.1.var: Expecting ',' delimiter: line 20 column 4 (char 774)
ERROR: C:\Games\VaM\AddonPackages\TiSeb.1Packanimation.1.var: Invalid control character at: line 11 column 134 (char 440)
ERROR: C:\Games\VaM\AddonPackages\ValiX.Snake_text.1.var: "There is no item named 'meta.json' in the archive"
 
The Amaimon.Bhaviour.2.var one is odd, to say the least. How does VaM even read a json that has "\XXX manage hands"\ written inside the description? It seems like in this one, the creator wanted to escape the " and somehow placed the \ on the wrong side. But why would VaM accept that when making the meta.json?
 
I think these vars are not your problem to solve. I can whack the ridiculous descriptions out of vars that have them. Some of the others appear to have no closing brace } at the end. I use Vim as my editor. It has JS syntax checking. I can use that to fix them inside the var without unzipping it. The one with no meta.json is just a texture pack, but I'll probably unpack that into Custom/Atom/Person so I can use it.
 
I think these vars are not your problem to solve. I can whack the ridiculous descriptions out of vars that have them. Some of the others appear to have no closing brace } at the end. I use Vim as my editor. It has JS syntax checking. I can use that to fix them inside the var without unzipping it. The one with no meta.json is just a texture pack, but I'll probably unpack that into Custom/Atom/Person so I can use it.
Well, you say that, but I couldn't help myself. I think I managed to squash the problems with these regex. But if this isn't enough, it probably isn't worth the hassle to try to catch every edge case of bad json.

Python:
# Clean up the file for invalid escapes, quotation marks, trailing commas and add missing closing bracket
def load(file):
    raw_data = file.read().decode("utf-8")
    cleaned_raw_data = re.sub(r'(\\)+(?![bfnrtv\"\'\\])', '', raw_data)
    cleaned_invalid_raw_data = re.sub(r'([^{\\\n\t: ] *)"(\s*\w)', r'\1\2', cleaned_raw_data)
    clean_trailing_comma = re.sub(r',(\s*})', r'\1', cleaned_invalid_raw_data)
    place_missing_end = re.sub(r',\s*$', r'}', clean_trailing_comma)
    data = loads(place_missing_end)
    return data

You can try this to see if it catches them all.

Edit: deleted code that just made it worse
 
Last edited:
That made things worse, actually. I think you should back out the last change and leave it the way it was. At least this way, people who use it will get errors from badly formatted vars that might cause other problems.
 
That made things worse, actually. I think you should back out the last change and leave it the way it was. At least this way, people who use it will get errors from badly formatted vars that might cause other problems.
Yeah, I think I'll just follow that advice.
 
Back
Top Bottom