5

I'm working to the following little project: https://github.com/AndreaCrotti/project-organizer

Which in short aims to manage many different projects much more easily. One of the useful things is a way to detect automatically the kind of project that I'm working on, to set up correctly some commands.

At the moment I'm using a classmethod "match" function and a detect function which iterates over the various "match". I'm sure there might be a better design for this, but can't find it.

Any ideas?

class ProjectType(object):
    build_cmd = ""

    @classmethod
    def match(cls, _):
        return True


class PythonProject(ProjectType):
    build_cmd = "python setup.py develop --user"

    @classmethod
    def match(cls, base):
        return path.isfile(path.join(base, 'setup.py'))


class AutoconfProject(ProjectType):
    #TODO: there should be also a way to configure it
    build_cmd = "./configure && make -j3"

    @classmethod
    def match(cls, base):
        markers = ('configure.in', 'configure.ac', 'makefile.am')
        return any(path.isfile(path.join(base, x)) for x in markers)


class MakefileOnly(ProjectType):
    build_cmd = "make"

    @classmethod
    def match(cls, base):
        # if we can count on the order the first check is not useful
        return (not AutoconfProject.match(base)) and \
            (path.isfile(path.join(base, 'Makefile')))


def detect_project_type(path):
    prj_types = (PythonProject, AutoconfProject, MakefileOnly, ProjectType)
    for p in prj_types:
        if p.match(path):
            return p()

2 Answers 2

5

This was a reasonable use of a factory function as a class method.

One possible improvement is to have all the classes inherit from a single parent class which would have a single classmethod that incorporated all the logic in detect_project_type.

Perhaps something like this would work:

class ProjectType(object):
    build_cmd = ""
    markers = []

    @classmethod
    def make_project(cls, path):
        prj_types = (PythonProject, AutoconfProject, MakefileOnly, ProjectType)
        for p in prj_types:
            markers = p.markers
            if any(path.isfile(path.join(path, x)) for x in markers):
                return p()

class PythonProject(ProjectType):
    build_cmd = "python setup.py develop --user"
    markers = ['setup.py']

class AutoconfProject(ProjectType):
    #TODO: there should be also a way to configure it
    build_cmd = "./configure && make -j3"
    markers = ['configure.in', 'configure.ac', 'makefile.am']

class MakefileOnly(ProjectType):
    build_cmd = "make"
    markers = ['Makefile']
Sign up to request clarification or add additional context in comments.

2 Comments

At this point I think I would let make_project sit outside the classes as just a plain function. It allows the naming of everything to be a bit more natural IMO. Also, while "the complexity has to go somewhere", I don't think I like the idea that the tuple of all child classes is information held within the parent class.
Thank really neat. I would also agree to have make_project outside, also because we are not really using the "cls" argument to make_project anyway..
3

To me this looks fine, though i would do two improvements

1- Use metaclass to automatically collect all ProjectTypes instead of manually listing them, that will avoid missing some project type by mistake or having a wrong order e.g.

class ProjectTypeManger(type):
    klasses = []
    def __new__(meta, classname, bases, classDict):
        klass = type.__new__(meta, classname, bases, classDict)
        meta.klasses.append(klass)
        return klass

    @classmethod
    def detect_project_type(meta, path):
        for p in meta.klasses:
            if p.match(path):
                return p()

class ProjectType(object):
    __metaclass__ = ProjectTypeManger
    build_cmd = ""

    @classmethod
    def match(cls, _):
        return None

2- match method should return the object itself instead of true/false, that way class can configure object anyway it wants + you can call baseclass match method, e.g. MakefileOnly can derive from AutoconfProject so that it first checks base class for any matches, but not sure if such inheritance makes sense

class MakefileOnly(AutoconfProject):
    build_cmd = "make"

    @classmethod
    def match(cls, base):
        ret = super(MakefileOnly, cls).match(base)
        if ret is not None:
            return ret
        if path.isfile(path.join(base, 'Makefile')):
            return cls()

        return None

3 Comments

So instead of having to remember to put every class in the list in order, now we have to remember to mark every class with the metaclass? It's probably easier to get right overall, but seems just as easy to forget about one class entirely, plus the work is now distributed across several classes...
@Karl Knechtel you don't have to mark every class, only one, the base class ProjectType
Thanks I like this solution, however if I have only 3-4 classes it's more the code added for the metaclass than the actual gain... But I will surely use this trick whenever I have more classes.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.