HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Local source control

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
sourcelocalcontrol

Problem

I've written my own local "source control". Rather than using a commit-based system, when you're ready to release a version, you can run a command which will create a .zip copy of your source code, and it's saved to a versions folder.

How does it work?

Each "project" written with this has a specific file structure that looks like the below. A file structure like this is required in order for the source control to work.

/[Project Name]
/source
...

/versions
...

/info
changelog.txt
readme.txt


There are also three commands that are used. new, push, and changedir. Each command argument is double colon, ::, separated, and look something like the following:

  • new::[project Name]::[project info] - Create a new project.



  • push::[version name]::[version info] - Push a new version to the versions folder as a .zip file.



  • changedir::[directory] - Change to a new directory.



Concerns

There are a few things I'm concerned about here.

  • Have I designed this in a clear intuitive way? The current design right now feels clunky and hard to use.



  • Am I over-documenting things? While I love documentation, if this is too much, tips on documentation would be appreciated.



  • Am I correctly handling errors?



```
import os
import re
import shutil

def command_new_project(project_name: str, project_description: str):
"""Generate a new project.

This function generates a new project. A
project structure looks like this:

/[project name]
/source
...

/versions
...

/info
readme.txt
changelog.txt

Keyword arguments:
project_name -- The name of the project.
project_description -- A brief description of the project.
"""
os.makedirs("./{0}".format(project_name))
os.chdir("./{0}".format(project_name))

os.makedirs("./source")
os.makedirs("./versions")
os.maked

Solution

Obviously, the error handling needs work: you are swallowing FileNotFoundError, discarding invalid input, and generally ignoring all kinds of failures. (In my experience, adding good error handling easily doubles the programming effort required.) There is no quit or exit command, nor do you handle EOFError gracefully.

:: isn't exactly the most user-friendly token separator.

The docstrings are too verbose for my personal taste, and get in the way of the code.

The command dispatch mechanism could be smarter. To add a new command, you have to define a function and add a condition to execute_user_input(). Furthermore, you need to specify the number of expected arguments, repeating yourself. The linear search for a matching command is also inelegant by Python standards.

The solution for all of that, I believe, introspection. I've also used variable-length argument lists for clarity and to avoid the awkward command = tokenized_user_input[0]; arguments = tokenized_user_input[1:] assignments.

import inspect
import os
import re
import shutil

class Commands:
    @staticmethod
    def new(project_name, project_description):
        ...

    @staticmethod
    def push(version_number, version_description):
        ...

    @staticmethod
    def changedir(directory):
        """Change to a new directory."""
        try:
            os.chdir(directory)
        except FileNotFoundError:
            pass    # TODO: error handling

def execute_user_input(command, *arguments):
    """
    Execute command, if the command exists and the correct number of arguments
    have been given.
    """
    function = getattr(Commands, command, None)
    if function is None or command.startswith('_'):
        return  # TODO: error handling
    argspec = inspect.getargspec(function)
    if ( len(arguments) == len(argspec.args) or
         len(arguments) > len(argspec.args) and argspec.varargs ):
        function(*arguments)

def tokenize_user_input(user_input):
    """Tokenize user input into a list."""
    return re.split("\s*::\s*", user_input)

def main():
    while True:
        user_input = input("lsc> ")
        execute_user_input(*tokenize_user_input(user_input))

if __name__ == "__main__":
    main()

Code Snippets

import inspect
import os
import re
import shutil

class Commands:
    @staticmethod
    def new(project_name, project_description):
        ...

    @staticmethod
    def push(version_number, version_description):
        ...

    @staticmethod
    def changedir(directory):
        """Change to a new directory."""
        try:
            os.chdir(directory)
        except FileNotFoundError:
            pass    # TODO: error handling


def execute_user_input(command, *arguments):
    """
    Execute command, if the command exists and the correct number of arguments
    have been given.
    """
    function = getattr(Commands, command, None)
    if function is None or command.startswith('_'):
        return  # TODO: error handling
    argspec = inspect.getargspec(function)
    if ( len(arguments) == len(argspec.args) or
         len(arguments) > len(argspec.args) and argspec.varargs ):
        function(*arguments)

def tokenize_user_input(user_input):
    """Tokenize user input into a list."""
    return re.split("\s*::\s*", user_input)


def main():
    while True:
        user_input = input("lsc> ")
        execute_user_input(*tokenize_user_input(user_input))


if __name__ == "__main__":
    main()

Context

StackExchange Code Review Q#98409, answer score: 4

Revisions (0)

No revisions yet.