5
\$\begingroup\$

I've created a heavily problematic code that works.

Summary: It looks through a folder structure, for png files and tries to replicate this structure in Photoshop. Files are mapped layers and Folders to Groups. I am using comtypes to access Photoshop and create the groups and layers.

The issue I faced with, is that you can't access Photoshop Layer Objects directly (instead you have to go through the whole structure). E.g. to access

new
 |
 -- folder
        |-- sub_folder
                     |-- random_file

in order to access the random_file, you have to access new, then folder, then sub_folder!

The following code works and it was developed through an intense mental state of concentration (and some trial and error). I believe it's horrendous though! As I am trying to learn how to be a better programmer and learn from the best, I though to ask your help: Can somebody guide me in order to re-factor this beast? What can I do better and what's the way to go with?

# Algorithm (?):
# ---------
# Starting from given top folder structure .
# traverse downwards and create
# photoshop groups named as the folders
# import all the png files under this
# as linked layers and name them accordingly
# traverse another layer deeper
# and do the same

# Import layers from Misc as Background and Shadow

os.chdir(ROOT_PATH + '/Misc')
import_all_pngs_as_layers(new_doc, new_doc, ROOT_PATH + '/Misc')
os.chdir(ROOT_PATH)  # Revert the working path

duplicate = False
subdir = False

for root, dd, ff in os.walk('.'):
    path = root.split(os.sep)
    if not os.path.basename(root) == '.':  # ignore parent
        if os.path.dirname(root).replace(".\\", "") in IGNORED_DIRS:
            pass
        elif not os.path.dirname(root) == '.' and not os.path.dirname(
                root).replace(".\\", "") in IGNORED_DIRS:
            # print('I am a subdir {} of the dir {}'.format(
            # os.path.basename(root),
            # os.path.dirname(root).replace(".\\", "")))
            create_group_named(
                new_doc, os.path.basename(root),
                new_doc.LayerSets(os.path.dirname(root).replace(".\\", "")))
        elif not os.path.basename(root) in IGNORED_DIRS:
            # print("Create TOP LEVEL layer group named",
            # os.path.basename(root))
            create_group_named(
                new_doc,
                os.path.basename(root))  # Create a group named 'subdir'
    if len(ff) > 1:
        for filename in ff:
            if filename.endswith('.png'):
                for item in GROUPED_LAYERS:
                    # print(item)
                    # print(item in filename)
                    if item in filename:
                        # print(
                        # 'lets create a group {} and put the layer{} under it in folder {}'
                        # .format(item, filename, os.path.basename(root)))
                        os.chdir(os.path.realpath(root))

                        try:
                            new_doc.LayerSets(
                                os.path.basename(root)).LayerSets(item)
                        except:
                            ng = create_group_named(
                                new_doc, item,
                                new_doc.LayerSets(os.path.basename(root)))
                            create_layer_from_file(
                                new_doc, filename, ng,
                                os.path.realpath(filename))

                        else:
                            # print(new_doc.LayerSets(os.path.basename(root)))
                            create_layer_from_file(
                                new_doc, filename,
                                new_doc.LayerSets(
                                    os.path.basename(root)).LayerSets(item),
                                os.path.realpath(filename))

                        duplicate = True
                        os.chdir(ROOT_PATH)
                if duplicate:
                    pass
                    duplicate = False
                else:
                    os.chdir(os.path.realpath(root))
                    # print('Rest files import as layers {} under {}'.format(
                    # filename, os.path.basename(root)))

                    if os.path.basename(
                            root) in IGNORED_DIRS or os.path.dirname(
                                root).replace(".\\", "") in IGNORED_DIRS:
                        pass
                    elif not os.path.dirname(root) == '.':
                        # print('layer {} on main group {} on group {}'
                        # .format(filename, os.path.dirname(root).replace(".\\",""), os.path.basename(root)))
                        create_layer_from_file(
                            new_doc, filename,
                            new_doc.LayerSets(
                                os.path.dirname(root).replace(
                                    ".\\", "")).LayerSets(
                                        os.path.basename(root)),
                            os.path.realpath(filename))
                    else:
                        create_layer_from_file(
                            new_doc, filename,
                            new_doc.LayerSets[os.path.basename(root)],
                            os.path.realpath(filename))
                    os.chdir(ROOT_PATH)
            else:
                pass

For completeness here's the rest of the program:

import comtypes.client as ct

psApp = ct.CreateObject('Photoshop.Application')
new_doc = psApp.Documents.Add(600, 800, 72, "new-psb-test", 2, 1, 1)

# they are ignored as those are the directories on the root level
# and I explicitly import them and their layers before the loop
IGNORED_DIRS = ['Misc', 'Wheel_Merged']

# If the files (inside the group/folder Accessories)
# include the following in their names then they need to be 
# sub- grouped under that name: e.g. Grille_01, 22_Grille, Grille0 all need to be
# layers under the Group named Grille 
GROUPED_LAYERS = ['Body_Side', 'Grille']

def create_group_named(doc, layer_set, under=''):
    """ Create a New LayerSet (aka Group) in the Photoshop
        document (doc).
    Args:
    doc (obj): The Photoshop Document Instance
    under (obj): The Group Object Instance
    (e.g. if you want a subgroup under Lights then give that
    object as a under name)
    layer_set (str): The name of the new Layer Set
    Returns:
    new_layer_set (obj): The LayerSet (Group) Object
    """
    if not under:  # add a top level group
        new_layer_set = doc.layerSets.Add()
    else:  # add subgroup
        new_layer_set = under.LayerSets.Add()
    new_layer_set.name = layer_set
    return new_layer_set

def paste_file_as_linked_layer(path):
    """ Import a file as a photoshop (smart) linked layer
    Args:
    path(str): The exact path of the image including extension
    Returns:
    whatever execute action returns (TBC)
    """
    idPlc = psApp.charIDToTypeID("Plc ")

    desc11 = ct.CreateObject("Photoshop.ActionDescriptor")
    idIdnt = psApp.charIDToTypeID("Idnt")
    desc11.putInteger(idIdnt, 2)

    # Open the file (path)
    idnull = psApp.charIDToTypeID("null")
    desc11.putPath(idnull, path)

    # set its type as a linked payer
    idLnkd = psApp.charIDToTypeID("Lnkd")
    desc11.putBoolean(idLnkd, True)

    idFTcs = psApp.charIDToTypeID("FTcs")
    idQCSt = psApp.charIDToTypeID("QCSt")
    idQcsa = psApp.charIDToTypeID("Qcsa")
    desc11.putEnumerated(idFTcs, idQCSt, idQcsa)

    idOfst = psApp.charIDToTypeID("Ofst")
    desc12 = ct.CreateObject('Photoshop.ActionDescriptor')
    idHrzn = psApp.charIDToTypeID("Hrzn")
    idRlt = psApp.charIDToTypeID("#Rlt")
    desc12.putUnitDouble(idHrzn, idRlt, 0)
    idVrtc = psApp.charIDToTypeID("Vrtc")
    idRlt = psApp.charIDToTypeID("#Rlt")
    desc12.putUnitDouble(idVrtc, idRlt, 0)
    idOfst = psApp.charIDToTypeID("Ofst")
    # put the object in an offset space of 0,0
    desc11.putObject(idOfst, idOfst, desc12)

    # 'return' of the function
    # is the placement of the linked layer
    f = psApp.executeAction(idPlc, desc11, 3)

def create_layer_from_file(doc, layer_name, layer_set, path):
    """ Create new Layer from File nested under a LayerSet
    Args:
    doc (obj): The working Photoshop file
    layer_name (str): The given name for the Layer
    layer_set (obj): the LayerSet object that the Layer is nested under
    path (str): the full Path of the file (including the extension)
    Returns:
    """
    psApp.activeDocument = doc
    layer = layer_set.artLayers.Add()
    layer.name = layer_name  # Rename Layer
    doc.activeLayer = layer  # Select Layer

    paste_file_as_linked_layer(os.path.realpath(path).replace('\\', '/'))

    return layer
\$\endgroup\$
  • 1
    \$\begingroup\$ @wizofe your code is not horrible, sure there's some formatting that would improve readability, and argparse would allow for extending into a full command line utility, aside from those nits it's looking okay for the most part. In particular, those if os.path.basename(...).replace(... lines maybe easier to debug later if it where a convenience function. As for accessing random_file within sub_folder, if ya don't like nesting, maybe try an intermediate structure to organize things the way ya like, eg. pandas, dict, etc. \$\endgroup\$ – S0AndS0 Apr 25 at 15:19
  • 2
    \$\begingroup\$ @S0AndS0: That sounds like the start of a nice answer! \$\endgroup\$ – Graipher Apr 26 at 9:55
  • \$\begingroup\$ Thanks @S0AndS0. Indeed I need to use argparse as I did in a similar tool I developed after this :) Can you give me examples for the terms: - 'convenience function': How I can implement one for the if os.path.basename(...).replace(...)? - My big trouble was to create such an 'intermediate structure' for my pre-existing folder structure/schema. An example on that would be very awesome (I am happy to see one both using pandas and a dict Thanks again :) \$\endgroup\$ – wizofe Apr 26 at 10:51
  • 1
    \$\begingroup\$ @Graipher some may call it foreshadowing ;-)... @wizofe most welcome and I'm glad that it seems to be helping so far, one thing my answer here does not cover, searching and accessing some random_file object, is something that I kinda covered over on math.stack. Though there'd be some modification I believe the resulting code maybe shorter than what's demonstrated there. Hint first_to_compute would instead contain a search string or list, maybe walking in a yielding fashion; bonus points for readable recursiveness. \$\endgroup\$ – S0AndS0 Apr 27 at 21:07
4
\$\begingroup\$

I spent sometime with the code you've currently posted @wizofe, I'm not sure if it'll ever be the same again...

Side note; I heard it rumored that the character limits are just a bit more relaxed than on other sub-stacks... or in other-words this may get a bit verbose, or in other-other-words it could be another one of those posts so a snack and drink is a solid choice.

utils_photoshop/__init__.py

I didn't have insight as to how you where organizing code so I'm making up file and directory names for later pseudo-importing.

#!/usr/bin/env python

import comtypes.client as ct


__license__ = """
See selection from author of question: https://codereview.stackexchange.com/q/219028/197446
"""


## Note from S0AndS0; repeated `new_doc` __will__ cause all manor of headaches
#def import_all_pngs_as_layers(new_doc, new_doc, path):
def import_all_pngs_as_layers(new_doc, path):
    """
    # Warning: this function is missing from current version of the posted question
    """
    pass


def create_named_group(doc, layer_set, under = ''):
    """
    Create a New LayerSet (aka Group) in the Photoshop document (doc).

    ## Arguments

    - doc (obj): The Photoshop Document Instance
    - under (obj): The Group Object Instance
    (e.g. if you want a subgroup under Lights then give that
    object as a under name)
    - layer_set (str): The name of the new Layer Set

    ## Returns: new_layer_set (obj): The LayerSet (Group) Object
    """
    if not under:  # add a top level group
        new_layer_set = doc.layerSets.Add()
    else:          # add subgroup
        new_layer_set = under.LayerSets.Add()

    new_layer_set.name = layer_set
    return new_layer_set


def paste_file_as_linked_layer(path, psApp):
    """
    Import a file as a Photoshop (smart) linked layer

    ## Arguments

    - path (str): The exact path of the image including extension
    - psApp (object): an instance of `ct.CreateObject('Photoshop.Application')`

    ## Returns: whatever execute action returns (TBC)
    """
    idPlc = psApp.charIDToTypeID('Plc ')

    desc11 = ct.CreateObject('Photoshop.ActionDescriptor')
    idIdnt = psApp.charIDToTypeID('Idnt')
    desc11.putInteger(idIdnt, 2)

    # Open the file (path)
    idnull = psApp.charIDToTypeID('null')
    desc11.putPath(idnull, path)

    # set its type as a linked payer
    idLnkd = psApp.charIDToTypeID('Lnkd')
    desc11.putBoolean(idLnkd, True)

    idFTcs = psApp.charIDToTypeID('FTcs')
    idQCSt = psApp.charIDToTypeID('QCSt')
    idQcsa = psApp.charIDToTypeID('Qcsa')
    desc11.putEnumerated(idFTcs, idQCSt, idQcsa)

    idOfst = psApp.charIDToTypeID('Ofst')
    desc12 = ct.CreateObject('Photoshop.ActionDescriptor')
    idHrzn = psApp.charIDToTypeID('Hrzn')
    idRlt = psApp.charIDToTypeID('#Rlt')
    desc12.putUnitDouble(idHrzn, idRlt, 0)
    idVrtc = psApp.charIDToTypeID('Vrtc')
    idRlt = psApp.charIDToTypeID('#Rlt')
    desc12.putUnitDouble(idVrtc, idRlt, 0)
    idOfst = psApp.charIDToTypeID('Ofst')
    # put the object in an offset space of 0,0
    desc11.putObject(idOfst, idOfst, desc12)

    # _`return`_ of this function is placement of the linked layer
    f = psApp.executeAction(idPlc, desc11, 3)


def create_layer_from_file(doc, layer_name, layer_set, path, psApp):
    """
    Create new Layer from File nested under a LayerSet

    ## Arguments

    - doc (obj): The working Photoshop file
    - layer_name (str): The given name for the Layer
    - layer_set (obj): the LayerSet object that the Layer is nested under
    - path (str): the full Path of the file (including the extension)
    - psApp (object): an instance of `ct.CreateObject('Photoshop.Application')`

    ## Returns: `layer`, an instance of `layer_set.artLayers.Add()`
    """
    psApp.activeDocument = doc
    layer = layer_set.artLayers.Add()
    layer.name = layer_name  # Rename Layer
    doc.activeLayer = layer  # Select Layer

    paste_file_as_linked_layer(os.path.realpath(path).replace('\\', '/'), psApp)

    return layer


def scrubbed_dirname(path):
    """
    ## Returns: path with any `.\\` removed
    """
    ## Note from S0AndS0; careful with writing code like this,
    ##  it can _spaghettify_ a code-base to incomprehensible levels.
    ##  Generally this is a sign that there is something that could
    ##  be done better with whatever is using functions like these.
    ##  Or in other-words, consider Ctrl^f "scrubbed_dirname" to be
    ##  a _highlighter_ for places that code may be getting _hairy_.
    ##  However, this is a balance of what is worth the time as well
    ##  as how far feature-creep plans to take this project.
    return os.path.dirname(path).replace('.\\', '')


def notice(msg, verbosity = 0, *f_list, **f_kwargs):
    """
    Prints formatted notices if `--verbosity` is greater than `0`

    ## Arguments

    - msg (str): The string to print, possibly with formatting
    - f_list (list): A list of arguments to pass to `format()`
    - f_kwargs (dict): Key word arguments to pass to `format()`

    ## Returns: None
    """
    ## Note from S0AndS0; when ya get to the point that adding
    ##  checks for other levels of verbosity, or maybe are considering
    ##  passing something like `v_threshold` and `v_current`, it might
    ##  be better to look for libraries that better express such intent.
    if verbosity <= 0:
        return

    if f_list and f_kwargs:
        print(msg.format(*f_list, **f_kwargs))
    elif f_list:
        print(msg.format(*f_list))
    elif f_kwargs:
        print(msg.format(**f_kwargs))
    else:
        print(msg)

As noted the import_all_pngs_as_layers function was missing from your question, not sure what it's special sauce is but that's okay as my internal parser is more tolerant than Python's. If this omission was intentional I'd advise editing in a real __license__ reference that expresses your views on edits, use, etc. instead of keeping the herbs-n-spices a secret.

The paste_file_as_linked_layer and create_layer_from_file functions received edits to allow for passing about psApp, I get that this is not ideal but more on that later.

The scrubbed_dirname is the convenience function that I commented about earlier, and much like the above code's comment-block states, this is something to be very careful with! These kinds of functions can be much like tribbles, one or two isn't too bad but given time and feature-creep maneuvering through a code-base can become a slog.

The notice function is another example of a convenience function that replaces the commented print() statements with something that outputs conditionally. But like the commented block states, it's probably a good idea to look into a library if there's more features wanted out of something like that. I didn't because I'm not about to assume you'll want to (if allowed) pip install something just to use my suggestions, that and notice'll show some fancy argument passing in the next code block.

I may have messed-up your documentation styling @wizofe (mainly because I use MarkDown just about everywhere), so my apologies on that point. And except for otherwise commented within the code-blocks you've done very well in dividing up portions of the problem space into manageable chunks. Not a whole lot of ground-breaking I can do in other-words.


png_to_photoshop.py

Much like some of those pocket monsters, pressing b will cancel this evolution...

#!/usr/bin/env python

import sys
import argparse
import comtypes.client as ct
import warnings

from textwrap import dedent
from utils_photoshop import (
    create_layer_from_file,
    create_named_group,
    import_all_pngs_as_layers,
    notice,
    paste_file_as_linked_layer,
    scrubbed_dirname)


#
# _Boilerplate stuff_
#
__description__ = """
This script recursively traverses folder structure starting with given path and creates;

- Photoshop groups named as each sub-directory, within which are

    - all the png files linked to layers and name accordingly

> Sub-directories are treated similarly if found
"""

__license__ = """
See selection from author of question: https://codereview.stackexchange.com/q/219028/197446
"""

__examples__ = """
Add PNG files from: /home/bill/Pictures/funny-cats/

    png_to_photoshop.py --basedir /home/bill/Pictures/funny-cats/\\
     --img_ext png\\
     --misc_dir YarnCollection\\
     --ignored_dirs YarnCollection Unicorns\\
     --layers_group costumed surprised adorbs\\
     --verbosity 9000
"""

__author__ = ('wizofe', 'https://codereview.stackexchange.com/users/199289/wizofe')

## Note to editors, please add yourself to the following
__editors__ = [
    ('S0AndS0', 'https://codereview.stackexchange.com/users/197446/s0ands0'),
]

## Note from S0AndS0; you will usually see the reverse of the following
##  at the bottom of files that maybe run or imported, in this case I am
##  being explicit as to how this file may be used without modification.
if __name__ != '__main__':
    raise NotImplementedError("Try running as a script, eg. python file-name.py --help")


#
# Parse the command line arguments into an `args_dict`
#
parser = argparse.ArgumentParser(description = __description__, epilog = __examples__, allow_abbrev = False)
## Example of adding command-line argument
# parser.add_argument('--foo', default = 'Fooed', type = str, help = 'Foo is defaulted to -> %(default)s')

parser.add_argument('--about', action = 'store_true', help = 'Prints info about this script and exits')
parser.add_argument('--license', action = 'store_true', help = 'Prints script license and exits')

parser.add_argument('--verbosity',
                    type = int,
                    default = 0,
                    help = "How verbose to be during execution, default %(default)s")

parser.add_argument('--img_ext',
                    default = 'png',
                    help = "Image extension to import into Photoshop with this script, default %(default)s")

parser.add_argument('--base_dir',
                    required = True,
                    help = 'Base directory to recursively parse for PNG files into Photoshop layers')

parser.add_argument('--misc_dir',
                    required = True,
                    help = 'Base directory to recursively parse for PNG files into Photoshop layers')

parser.add_argument('--ignored_dirs',
                    nargs='+',
                    default = ['Misc', 'Wheel_Merged'],
                    help = "List of sub-directory names to ignore while _walking_ base_dir, default %(default)s")

## Note from S0AndS0; ya may want to play with the following `help`
##  to get output of `script-name.py --help` looking more acceptable.
parser.add_argument('--layers_group',
                    nargs='+',
                    default = ['Body_Side', 'Grille'],
                    help = textwrap.dedent("""
                        List of association names between file names, and Photoshop sub-groups, default %(default)s.

                        For example "{name}_01, 22_{name}, {name}0" could be grouped to "{name}",
                        via something like...

                            script-name.py --layers_group {name}
                    """.format(name = 'Grille')))

args_dict = vars(parser.parse_args())


#
# Ways to prematurely exit from script, note `--help` takes care of itself
#
if args_dict['license']:
    print(__license__)
    sys.exit()


if args_dict['about']:
    message = """
    This script and related code was brought to you by

    Author: {name} of {url}
    """.format(name = __author__[0], url = __author__[1])

    if __editors__:
        message += '\n\n\tand...\n'
    for name, url in __editors__:
        message += "\t{name} -> {url}".format(**{'name': name, 'url': url})

    print(dedent(message))
    sys.exit()


## Note from S0AndS0; I moved the following two assignments and
##  modified the `paste_file_as_linked_layer` and `create_layer_from_file`
##  functions to pass `psApp` about, because I figure a future self
##  or reader will want to customize it a bit more. This is less than
##  ideal, so see the posted notes for some thoughts on the future.
psApp = ct.CreateObject('Photoshop.Application')
new_doc = psApp.Documents.Add(600, 800, 72, 'new-psb-test', 2, 1, 1)

# Import layers from Misc as Background and Shadow
misc_path = os.path.join(args_dict['base_dir'], args_dict['misc_dir'])
os.chdir(misc_path)
import_all_pngs_as_layers(new_doc = new_doc, path = misc_path)
os.chdir(args_dict['base_dir'])  # Revert the working path

duplicate = False
subdir = False
img_ext = ".{ext}".format(ext = args_dict['img_ext'])


for root, dd, ff in os.walk(args_dict['base_dir']):
    ## Note from S0AndS0; where is `path` being used?
    ##  Or is this _zombie_/_vestigial_ code?
    path = root.split(os.sep)

    root_dir_basename = os.path.basename(root)
    if root_dir_basename == '.':  # ignore parent
        pass

    if scrubbed_dirname(root) in args_dict['ignored_dirs']:
        pass

    if not os.path.dirname(root) == '.' and not scrubbed_dirname(root) in args_dict['ignored_dirs']:
        notice(msg = "I am a subdir {} of the dir {}",
               verbosity = args_dict['verbosity'],
               root_dir_basename,
               scrubbed_dirname(root))
        create_named_group(
            doc = new_doc,
            layer_set = root_dir_basename,
            under = new_doc.LayerSets(scrubbed_dirname(root)))
    elif not root_dir_basename in args_dict['ignored_dirs']:
        notice(msg = "Creating TOP LEVEL layer group {name}",
               verbosity = args_dict['verbosity'],
               name = root_dir_basename)
        create_named_group(
            doc = new_doc,
            layer_set = root_dir_basename)
    else:  # Uncaught state!
        warnings.warn("\n".join([
            'How did I get here?',
            "\tWhat did you give me?",
            "\t... uh-oh, I think I will pass on these questions and current state with...",
            "\troot_dir_basename -> {path}".format(path = root_dir_basename),
        ]))
        pass

    ## Note from S0AndS0; the following might be better if higher
    ##  up in the execution stack of for loop, unless there is a
    ##  reason to have `create_named_group` fire on empty `ff`
    if len(ff) <= 0:
        pass

    for filename in ff:
        if not filename.endswith(img_ext):
            pass

        for item in args_dict['layers_group']:
            notice(msg = "{item} in {filename}",
                   verbosity = args_dict['verbosity'],
                   item = item,
                   filename = filename)
            if item in filename:
                notice(msg = "Creating group {group} to place layer {layer} within folder {folder}",
                       verbosity = args_dict['verbosity'],
                       group = item,
                       layer = filename,
                       folder = root_dir_basename)
                os.chdir(os.path.realpath(root))

                try:
                    new_doc.LayerSets(root_dir_basename).LayerSets(item)
                except:
                    named_group = create_named_group(
                        doc = new_doc,
                        layer_set = item,
                        under = new_doc.LayerSets(root_dir_basename))
                    create_layer_from_file(
                        doc = new_doc,
                        layer_name = filename,
                        layer_set = named_group,
                        path = os.path.realpath(filename),
                        psApp = psApp)
                else:
                    notice(msg = new_doc.LayerSets(root_dir_basename), verbosity = args_dict['verbosity'])
                    create_layer_from_file(
                        doc = new_doc,
                        layer_name = filename,
                        layer_set = new_doc.LayerSets(root_dir_basename).LayerSets(item),
                        path = os.path.realpath(filename),
                        psApp = psApp)

                duplicate = True
                os.chdir(args_dict['base_dir'])

        if duplicate:
            duplicate = False
            pass

        if root_dir_basename in args_dict['ignored_dirs'] or scrubbed_dirname(root) in args_dict['ignored_dirs']:
            pass

        os.chdir(os.path.realpath(root))
        notice(msg = "Resting files imported as layers {layer} under {folder}",
               verbosity = args_dict['verbosity'],
               layer = filename,
               folder = root_dir_basename)

        if not os.path.dirname(root) == '.':
            notice(msg = "layer {layer} on main group {m_group} on group {group}",
                   verbosity = args_dict['verbosity'],
                   layer = filename,
                   m_group = os.path.dirname(root).replace('.\\', ''),
                   group = root_dir_basename)
            create_layer_from_file(
                doc = new_doc,
                layer_name = filename,
                layer_set = new_doc.LayerSets(scrubbed_dirname(root)).LayerSets(root_dir_basename),
                path = os.path.realpath(filename),
                psApp = psApp)
        else:
            create_layer_from_file(
                doc = new_doc,
                layer_name = filename,
                layer_set = new_doc.LayerSets[root_dir_basename],
                path = os.path.realpath(filename),
                psApp = psApp)

            os.chdir(args_dict['base_dir'])

There'll be somethings that'll likely look familiar in the above code block, and some things that are different; I'll encourage readers to pull-out interesting bits and try'em in scriptlets because as it is neither the question's code nor what I've so far posted will function.

If there are questions on why I've written some-thing some-way, feel free to ask in the comments. Basically all I've added so far is some argparse tricks and re-factored your code to be a little less cyclomaticly complex (it's still a bit high as it is), really you where on the right track @wizofe with using pass often and I've just tried to follow along with it.

The observant readers may also be noting my use of quotes, while Python doesn't care one way or another on 'some string' vs "some string", I personally find it easier to read moons later when there's consistent use that means something about the contents. In this case single-quotes don't have anything fancy going on where as double quotes may have formatting stuffed in somewhere.


Moving on to what could be further improved, and on track with what I was commenting earlier about and intermediate structure for organizing some of this. Some of these for loops might be easier to expand upon as Iterators, eg...

#!/usr/bin/env python

import sys
import argparse
import comtypes.client as ct
import warnings

from textwrap import dedent
from utils_photoshop import (
    create_layer_from_file,
    create_named_group,
    import_all_pngs_as_layers,
    notice,
    paste_file_as_linked_layer,
    scrubbed_dirname)

from collections import Iterator


class DirWalker_PhotoshopRanger(Dict, Iterator):
    """
    DirWalker_PhotoshopRanger is a hybrid class between a `dict`ionary and `Iterator`

    After modifications your _mileage_ may be improved
    """

    def __init__(base_dir, misc_dir, layers_group, ignored_dirs, verbosity = 0, **kwargs):
        super(DirWalker_PhotoshopRanger, self).__init__(**kwargs)
        self.update({
            'base_dir': base_dir,
            'misc_dir': misc_dir,
            'layers_group': layers_group,
            'ignored_dirs': ignored_dirs,
            'verbosity': verbosity},
            'dir_steps': os.walk(base_dir),
            'duplicate': False,
            'subdir': False,
            'grouped_layers': {},
            'psApp': ct.CreateObject('Photoshop.Application'),
            'misc_path': os.path.join(base_dir, misc_dir))

        os.chdir(self['misc_path'])
        self.update(new_doc = self['psApp'].Documents.Add(600, 800, 72, 'new-psb-test', 2, 1, 1))
        self.update(misc_layers = import_all_pngs_as_layers(new_doc = self['new_doc'], path = self['misc_path']))
        # Revert the working path
        os.chdir(self['base_dir'])

        self.update()

    def __iter__(self):
        return self

    def next(self):
        try:
            root, dd, ff = self['dir_steps'].next()
        except (StopIteration):
            self.throw(GeneratorExit)

        if scrubbed_dirname(root) in self['ignored_dirs']:
            pass

        root_dir_basename = os.path.basename(root)
        if root_dir_basename == '.':  # ignore parent
            pass

        ## ... do more stuff before returning `self`

        return self

    #
    # Python 2/3 compatibility stuff
    #

    def throw(self, type = None, traceback = None):
        raise StopIteration

    __next__ = next

... which may give ya the controls to flatten things into categories (first sub-directory name, or similar) via something like self['categories'].update(hash_path: container_object) or self['categories'].append(container_object)

In the future you might want to consider pulling some of the functions from utils_photoshop/__init__.py into the DirWalker_PhotoshopRanger class. Namely those that I modified to pass psApp about, instead maybe hav'em use self['psApp'].

Incorporating portions of pre-existing code from the main for loop into the suggested iterator will require a little editing, eg. ...doc = new_doc,... would become ...doc = self['new_doc'],..., and later maybe removed if things like the create_layer_from_file function are pulled into the class. That last state might begin to look something like...

#!/usr/bin/env python

## ... other import and initialization stuff

class DirWalker_PhotoshopRanger(Dict, Iterator):

    # ... things like __init__ above

    def create_named_group(self, layer_set, under = ''):
        """
        Create a New LayerSet (aka Group) in the Photoshop document (self['new_doc']).

        ## Arguments

        - layer_set (str): The name of the new Layer Set
        - under (obj): The Group Object Instance, optional

        ## Returns: new_layer_set (obj): The LayerSet (Group) Object
        """
        if not under:  # add a top level group
            new_layer_set = self['new_doc'].layerSets.Add()
        else:          # add subgroup
            new_layer_set = under.LayerSets.Add()

        new_layer_set.name = layer_set
        return new_layer_set

    def next(self):
        try:
            root, dd, ff = self['dir_steps'].next()
        except (StopIteration):
            self.throw(GeneratorExit)

        root_dir_basename = os.path.basename(root)
        if root_dir_basename == '.':  # ignore parent
            pass

        if scrubbed_dirname(root) in self['ignored_dirs']:
            pass

        if not os.path.dirname(root) == '.' and not scrubbed_dirname(root) in self['ignored_dirs']:
            notice(msg = "Searching sub-directory {subdir} of the directory {directory}",
                   verbosity = self['verbosity'],
                   subdir = oot_dir_basename,
                   directory = scrubbed_dirname(root))
            self.create_named_group(
                layer_set = root_dir_basename,
                under = self['new_doc'].LayerSets(scrubbed_dirname(root)))

        ## ... do more stuff before returning `self`

        return self

    ## ... other class stuff


if __name__ == __main__:
    import argparse

    #
    # Parse the command line arguments into an `args_dict`
    #
    parser = argparse.ArgumentParser(description = __description__, epilog = __examples__, allow_abbrev = False)
    # parser.add_argument('--foo', default = 'Fooed', type = str, help = "Foo is defaulted to -> %(default)s")

    ## ... more argparsy goodness

    args_dict = vars(parser.parse_args())


    #
    # Initialize the DirWalker
    #
    f_line = ".-.".join('_' for _ in range(9))
    dir_walker = DirWalker_PhotoshopRanger(**args_dict)

    for i, state in enumerate(dir_walker):
        notice(msg = "{f_line}\n{count}".format(f_line = f_line, count = i),
               verbosity = 9001)
        for k, v in state.items():
            notice(msg = "{key} -> {value}".format(key = k, value = v),
                   verbosity = 9002)

        notice(msg = "{f_line}".format(f_line = f_line), verbosity = 9001)

If written carefully these structures can be very performant on memory use and other system resources. Though yet again I'll warn that a code-base can get hairy with'em, in different ways than with convenience functions, but with very similar effect. Additionally it maybe of use to know that functions that use yield operate very similarly to iterators, and be called generators, eg...

def range_summinator(start, end):
    increment = 1
    if end < start:
        increment = -1

    while start != end:
        start += increment

        yield start


r = range_summinator(0, 2)
r.next()  # -> 1
r.next()  # -> 2
r.next()
#   Traceback (most recent call last):
#     File "<stdin>", line 1, in <module>
#   StopIteration

Okay I think this is a good pausing point to mull things over, and perhaps find bugs within my own suggestions, gotta say it was kinda a challenge without testing it in it's entirety. I may update this as thoughts percolate, and if comments/questions are posted.


Resources useful in writing (and maybe expanding upon) above are not limited to the following;

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.