updates to project plugins:

- force projects to return unicode names and branches, or None
 - refactor ProjectMap to set project name from cfg file
 - pass configs to find_project() as dict from plugin's cfg section
 - clean up git project class

updates to config file:

 - rename ~/.wakatime.conf to ~/.wakatime.cfg
 - better error handling while parsing and getting configs
 - no longer need to set defaults when parsing config file
 - use configparser for python3 when python2 import fails
This commit is contained in:
Alan Hamlett 2013-12-13 14:46:39 +01:00
parent ce4d6ce3b7
commit 297ebb902b
8 changed files with 165 additions and 148 deletions

14
AUTHORS Normal file
View file

@ -0,0 +1,14 @@
WakaTime is written and maintained by Alan Hamlett and
various contributors:
Development Lead
----------------
- Alan Hamlett <alan.hamlett@gmail.com>
Patches and Suggestions
-----------------------
- 3onyc <3onyc@x3tech.com>

View file

@ -26,8 +26,15 @@ import re
import sys
import time
import traceback
from ConfigParser import RawConfigParser
try:
import ConfigParser as configparser
except ImportError:
import configparser
try:
from urllib2 import HTTPError, Request, urlopen
except ImportError:
from urllib.error import HTTPError
from urllib.request import Request, urlopen
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages'))
@ -37,11 +44,6 @@ from .stats import get_file_stats
from .packages import argparse
from .packages import simplejson as json
from .packages import tzlocal
try:
from urllib2 import HTTPError, Request, urlopen
except ImportError:
from urllib.error import HTTPError
from urllib.request import Request, urlopen
log = logging.getLogger(__name__)
@ -53,55 +55,63 @@ class FileAction(argparse.Action):
values = os.path.realpath(values)
setattr(namespace, self.dest, values)
def checkUpdateConfigFile(configFile):
"""Checks if the config has a header section, if not add it for ConfigParser"""
with open(configFile) as fh:
configData = fh.read()
if not configData.strip().startswith('[settings]'):
configData = "[settings]\n" + configData.strip()
with open(configFile, 'w') as fh:
fh.write(configData)
def upgradeConfigFile(configFile):
"""For backwards-compatibility, upgrade the existing config file
to work with configparser and rename from .wakatime.conf to .wakatime.cfg.
"""
if os.path.isfile(configFile):
# if upgraded cfg file already exists, don't overwrite it
return
oldConfig = os.path.join(os.path.expanduser('~'), '.wakatime.conf')
try:
with open(oldConfig) as infile:
with open(configFile, 'w') as outfile:
outfile.write("[settings]\n%s" % infile.read().strip())
os.remove(oldConfig)
except IOError:
pass
def parseConfigFile(configFile):
"""Returns a configparser.SafeConfigParser instance with configs
read from the config file. Default location of the config file is
at ~/.wakatime.cfg.
"""
if not configFile:
configFile = os.path.join(os.path.expanduser('~'), '.wakatime.conf')
configFile = os.path.join(os.path.expanduser('~'), '.wakatime.cfg')
checkUpdateConfigFile(configFile)
# define default config values
defaults = {
'settings' : {
'api_key': None,
'ignore': [],
'verbose': False
},
}
if not os.path.isfile(configFile):
return configs
upgradeConfigFile(configFile)
configs = configparser.SafeConfigParser()
try:
with open(configFile) as fh:
configs = RawConfigParser()
setConfigDefaults(configs, defaults)
configs.readfp(fh)
try:
configs.readfp(fh)
except configparser.Error:
print(traceback.format_exc())
return None
except IOError:
print('Error: Could not read from config file ~/.wakatime.conf')
if not os.path.isfile(configFile):
print('Error: Could not read from config file ~/.wakatime.conf')
return configs
def setConfigDefaults(config, defaults):
for section, values in defaults.iteritems():
if not config.has_section(section):
config.add_section(section)
for key, value in values.iteritems():
config.set(section, key, value)
def parseArguments(argv):
"""Parse command line arguments and configs from ~/.wakatime.cfg.
Command line arguments take precedence over config file settings.
Returns instances of ArgumentParser and SafeConfigParser.
"""
try:
sys.argv
except AttributeError:
sys.argv = argv
# define supported command line arguments
parser = argparse.ArgumentParser(
description='Wakati.Me event api appender')
parser.add_argument('--file', dest='targetFile', metavar='file',
@ -129,24 +139,41 @@ def parseArguments(argv):
parser.add_argument('--verbose', dest='verbose', action='store_true',
help='turns on debug messages in log file')
parser.add_argument('--version', action='version', version=__version__)
# parse command line arguments
args = parser.parse_args(args=argv[1:])
# use current unix epoch timestamp by default
if not args.timestamp:
args.timestamp = time.time()
# set arguments from config file
# parse ~/.wakatime.cfg file
configs = parseConfigFile(args.config)
if configs is None:
return args, configs
# update args from configs
if not args.key:
default_key = configs.get('settings', 'api_key')
default_key = None
if configs.has_option('settings', 'api_key'):
default_key = configs.get('settings', 'api_key')
if default_key:
args.key = default_key
else:
parser.error('Missing api key')
for pattern in configs.get('settings', 'ignore'):
if not args.ignore:
args.ignore = []
args.ignore.append(pattern)
if not args.ignore:
args.ignore = []
if configs.has_option('settings', 'ignore'):
try:
for pattern in configs.get('settings', 'ignore').split("\n"):
if pattern.strip() != '':
args.ignore.append(pattern)
except TypeError:
pass
if not args.verbose and configs.has_option('settings', 'verbose'):
args.verbose = configs.getboolean('settings', 'verbose')
if not args.verbose and configs.has_option('settings', 'debug'):
args.verbose = configs.getboolean('settings', 'debug')
if not args.logfile and configs.has_option('settings', 'logfile'):
args.logfile = configs.get('settings', 'logfile')
@ -244,28 +271,39 @@ def send_action(project=None, branch=None, stats={}, key=None, targetFile=None,
def main(argv=None):
if not argv:
argv = sys.argv
args, config = parseArguments(argv)
args, configs = parseArguments(argv)
if configs is None:
return 103 # config file parsing error
setup_logging(args, __version__)
ignore = should_ignore(args.targetFile, args.ignore)
if ignore is not False:
log.debug('File ignored because matches pattern: %s' % ignore)
return 0
if os.path.isfile(args.targetFile):
stats = get_file_stats(args.targetFile)
project = find_project(args.targetFile, configs=configs)
branch = None
name = None
stats = get_file_stats(args.targetFile)
project = find_project(args.targetFile, config)
if project:
branch = project.branch()
name = project.name()
project_name = project.name()
if send_action(
project=name,
project=project_name,
branch=branch,
stats=stats,
**vars(args)
):
return 0
return 102
return 0 # success
return 102 # api error
else:
log.debug('File does not exist; ignoring this action.')
return 101
return 0

View file

@ -12,15 +12,18 @@
import logging
import os
from .projects.wakatime import WakaTime
from .projects.projectmap import ProjectMap
from .projects.git import Git
from .projects.mercurial import Mercurial
from .projects.projectmap import ProjectMap
from .projects.subversion import Subversion
from .projects.wakatime import WakaTime
log = logging.getLogger(__name__)
# List of plugin classes to find a project for the current file path.
# Project plugins will be processed with priority in the order below.
PLUGINS = [
WakaTime,
ProjectMap,
@ -30,14 +33,13 @@ PLUGINS = [
]
def find_project(path, config):
def find_project(path, configs=None):
for plugin in PLUGINS:
plugin_name = plugin.__name__.lower()
if config.has_section(plugin_name):
plugin_config = config
else:
plugin_config = None
project = plugin(path, plugin_config)
plugin_configs = None
if configs and configs.has_section(plugin_name):
plugin_configs = dict(configs.items(plugin_name))
project = plugin(path, configs=plugin_configs)
if project.process():
return project
return None

View file

@ -22,19 +22,19 @@ class BaseProject(object):
be found for the current path.
"""
def __init__(self, path, settings):
def __init__(self, path, configs=None):
self.path = path
self.settings = settings
self._configs = configs
def type(self):
def project_type(self):
""" Returns None if this is the base class.
Returns the type of project if this is a
valid project.
"""
type = self.__class__.__name__.lower()
if type == 'baseproject':
type = None
return type
project_type = self.__class__.__name__.lower()
if project_type == 'baseproject':
project_type = None
return project_type
def process(self):
""" Processes self.path into a project and

View file

@ -25,35 +25,32 @@ log = logging.getLogger(__name__)
class Git(BaseProject):
def process(self):
self.config = self._find_config(self.path)
if self.config:
return True
return False
self.configFile = self._find_git_config_file(self.path)
return self.configFile is not None
def name(self):
base = self._project_base()
if base:
return os.path.basename(base)
return unicode(os.path.basename(base))
return None
def branch(self):
branch = None
base = self._project_base()
if base:
head = os.path.join(self._project_base(), '.git', 'HEAD')
try:
with open(head) as f:
branch = f.readline().strip().rsplit('/', 1)[-1]
with open(head) as fh:
return unicode(fh.readline().strip().rsplit('/', 1)[-1])
except IOError:
pass
return branch
def _project_base(self):
if self.config:
return os.path.dirname(os.path.dirname(self.config))
return None
def _find_config(self, path):
def _project_base(self):
if self.configFile:
return os.path.dirname(os.path.dirname(self.configFile))
return None
def _find_git_config_file(self, path):
path = os.path.realpath(path)
if os.path.isfile(path):
path = os.path.split(path)[0]
@ -62,34 +59,4 @@ class Git(BaseProject):
split_path = os.path.split(path)
if split_path[1] == '':
return None
return self._find_config(split_path[0])
def _parse_config(self):
sections = {}
try:
f = open(self.config, 'r')
except IOError as e:
log.exception("Exception:")
else:
with f:
section = None
for line in f.readlines():
line = line.lstrip()
if len(line) > 0 and line[0] == '[':
section = line[1:].split(']', 1)[0]
temp = section.split(' ', 1)
section = temp[0].lower()
if len(temp) > 1:
section = ' '.join([section, temp[1]])
sections[section] = {}
else:
try:
(setting, value) = line.split('=', 1)
except ValueError:
setting = line.split('#', 1)[0].split(';', 1)[0]
value = 'true'
setting = setting.strip().lower()
value = value.split('#', 1)[0].split(';', 1)[0].strip()
sections[section][setting] = value
f.close()
return sections
return self._find_git_config_file(split_path[0])

View file

@ -1,20 +1,16 @@
# -*- coding: utf-8 -*-
"""
wakatime.projects.projectmap
~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Information from ~/.waka-projectmap mapping folders (relative to home folder)
to project names
Use the ~/.wakatime.cfg file to define custom projects for folders.
:author: 3onyc
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import logging
import os
from functools import partial
from ..packages import simplejson as json
from .base import BaseProject
@ -23,34 +19,36 @@ log = logging.getLogger(__name__)
class ProjectMap(BaseProject):
def process(self):
if not self.settings:
if not self._configs:
return False
self.project = self._find_project()
return self.project != None
self.project = self._find_project(self.path)
def _find_project(self):
has_option = partial(self.settings.has_option, 'projectmap')
get_option = partial(self.settings.get, 'projectmap')
paths = self._path_generator()
return self.project is not None
projects = map(get_option, filter(has_option, paths))
return projects[0] if projects else None
def _find_project(self, path):
path = os.path.realpath(path)
if os.path.isfile(path):
path = os.path.split(path)[0]
if self._configs.get(path.lower()):
return self._configs.get(path.lower())
if self._configs.get('%s/' % path.lower()):
return self._configs.get('%s/' % path.lower())
if self._configs.get('%s\\' % path.lower()):
return self._configs.get('%s\\' % path.lower())
split_path = os.path.split(path)
if split_path[1] == '':
return None
return self._find_project(split_path[0])
def branch(self):
return None
def name(self):
return self.project
def _path_generator(self):
"""
Generates paths from the current directory up to the user's home folder
stripping anything in the path before the home path
"""
path = self.path.replace(os.environ['HOME'], '')
while path != os.path.dirname(path):
yield path
path = os.path.dirname(path)
if self.project:
return unicode(self.project)
return None

View file

@ -30,13 +30,12 @@ class Subversion(BaseProject):
return self._find_project_base(self.path)
def name(self):
return self.info['Repository Root'].split('/')[-1]
return unicode(self.info['Repository Root'].split('/')[-1])
def branch(self):
branch = None
if self.base:
branch = os.path.basename(self.base)
return branch
unicode(os.path.basename(self.base))
return None
def _get_info(self, path):
info = OrderedDict()

View file

@ -28,13 +28,12 @@ class WakaTime(BaseProject):
return False
def name(self):
project_name = None
try:
with open(self.config) as fh:
project_name = fh.readline().strip()
return unicode(fh.readline().strip())
except IOError as e:
log.exception("Exception:")
return project_name
return None
def branch(self):
return None