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 sys
import time import time
import traceback import traceback
try:
from ConfigParser import RawConfigParser 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.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages')) 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 argparse
from .packages import simplejson as json from .packages import simplejson as json
from .packages import tzlocal 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__) log = logging.getLogger(__name__)
@ -53,55 +55,63 @@ class FileAction(argparse.Action):
values = os.path.realpath(values) values = os.path.realpath(values)
setattr(namespace, self.dest, 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: def upgradeConfigFile(configFile):
fh.write(configData) """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): 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: if not configFile:
configFile = os.path.join(os.path.expanduser('~'), '.wakatime.conf') configFile = os.path.join(os.path.expanduser('~'), '.wakatime.cfg')
checkUpdateConfigFile(configFile) upgradeConfigFile(configFile)
# define default config values
defaults = {
'settings' : {
'api_key': None,
'ignore': [],
'verbose': False
},
}
if not os.path.isfile(configFile):
return configs
configs = configparser.SafeConfigParser()
try: try:
with open(configFile) as fh: with open(configFile) as fh:
configs = RawConfigParser() try:
setConfigDefaults(configs, defaults)
configs.readfp(fh) configs.readfp(fh)
except configparser.Error:
print(traceback.format_exc())
return None
except IOError: except IOError:
if not os.path.isfile(configFile):
print('Error: Could not read from config file ~/.wakatime.conf') print('Error: Could not read from config file ~/.wakatime.conf')
return configs 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): 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: try:
sys.argv sys.argv
except AttributeError: except AttributeError:
sys.argv = argv sys.argv = argv
# define supported command line arguments
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Wakati.Me event api appender') description='Wakati.Me event api appender')
parser.add_argument('--file', dest='targetFile', metavar='file', parser.add_argument('--file', dest='targetFile', metavar='file',
@ -129,24 +139,41 @@ def parseArguments(argv):
parser.add_argument('--verbose', dest='verbose', action='store_true', parser.add_argument('--verbose', dest='verbose', action='store_true',
help='turns on debug messages in log file') help='turns on debug messages in log file')
parser.add_argument('--version', action='version', version=__version__) parser.add_argument('--version', action='version', version=__version__)
# parse command line arguments
args = parser.parse_args(args=argv[1:]) args = parser.parse_args(args=argv[1:])
# use current unix epoch timestamp by default
if not args.timestamp: if not args.timestamp:
args.timestamp = time.time() args.timestamp = time.time()
# set arguments from config file # parse ~/.wakatime.cfg file
configs = parseConfigFile(args.config) configs = parseConfigFile(args.config)
if configs is None:
return args, configs
# update args from configs
if not args.key: if not args.key:
default_key = None
if configs.has_option('settings', 'api_key'):
default_key = configs.get('settings', 'api_key') default_key = configs.get('settings', 'api_key')
if default_key: if default_key:
args.key = default_key args.key = default_key
else: else:
parser.error('Missing api key') parser.error('Missing api key')
for pattern in configs.get('settings', 'ignore'):
if not args.ignore: if not args.ignore:
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) args.ignore.append(pattern)
except TypeError:
pass
if not args.verbose and configs.has_option('settings', 'verbose'): if not args.verbose and configs.has_option('settings', 'verbose'):
args.verbose = configs.getboolean('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'): if not args.logfile and configs.has_option('settings', 'logfile'):
args.logfile = configs.get('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): def main(argv=None):
if not argv: if not argv:
argv = sys.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__) setup_logging(args, __version__)
ignore = should_ignore(args.targetFile, args.ignore) ignore = should_ignore(args.targetFile, args.ignore)
if ignore is not False: if ignore is not False:
log.debug('File ignored because matches pattern: %s' % ignore) log.debug('File ignored because matches pattern: %s' % ignore)
return 0 return 0
if os.path.isfile(args.targetFile): if os.path.isfile(args.targetFile):
stats = get_file_stats(args.targetFile)
project = find_project(args.targetFile, configs=configs)
branch = None branch = None
name = None name = None
stats = get_file_stats(args.targetFile)
project = find_project(args.targetFile, config)
if project: if project:
branch = project.branch() branch = project.branch()
name = project.name() project_name = project.name()
if send_action( if send_action(
project=name, project=project_name,
branch=branch, branch=branch,
stats=stats, stats=stats,
**vars(args) **vars(args)
): ):
return 0 return 0 # success
return 102
return 102 # api error
else: else:
log.debug('File does not exist; ignoring this action.') log.debug('File does not exist; ignoring this action.')
return 101 return 0

View file

@ -12,15 +12,18 @@
import logging import logging
import os import os
from .projects.wakatime import WakaTime
from .projects.projectmap import ProjectMap
from .projects.git import Git from .projects.git import Git
from .projects.mercurial import Mercurial from .projects.mercurial import Mercurial
from .projects.projectmap import ProjectMap
from .projects.subversion import Subversion from .projects.subversion import Subversion
from .projects.wakatime import WakaTime
log = logging.getLogger(__name__) 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 = [ PLUGINS = [
WakaTime, WakaTime,
ProjectMap, ProjectMap,
@ -30,14 +33,13 @@ PLUGINS = [
] ]
def find_project(path, config): def find_project(path, configs=None):
for plugin in PLUGINS: for plugin in PLUGINS:
plugin_name = plugin.__name__.lower() plugin_name = plugin.__name__.lower()
if config.has_section(plugin_name): plugin_configs = None
plugin_config = config if configs and configs.has_section(plugin_name):
else: plugin_configs = dict(configs.items(plugin_name))
plugin_config = None project = plugin(path, configs=plugin_configs)
project = plugin(path, plugin_config)
if project.process(): if project.process():
return project return project
return None return None

View file

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

View file

@ -25,35 +25,32 @@ log = logging.getLogger(__name__)
class Git(BaseProject): class Git(BaseProject):
def process(self): def process(self):
self.config = self._find_config(self.path) self.configFile = self._find_git_config_file(self.path)
if self.config: return self.configFile is not None
return True
return False
def name(self): def name(self):
base = self._project_base() base = self._project_base()
if base: if base:
return os.path.basename(base) return unicode(os.path.basename(base))
return None return None
def branch(self): def branch(self):
branch = None
base = self._project_base() base = self._project_base()
if base: if base:
head = os.path.join(self._project_base(), '.git', 'HEAD') head = os.path.join(self._project_base(), '.git', 'HEAD')
try: try:
with open(head) as f: with open(head) as fh:
branch = f.readline().strip().rsplit('/', 1)[-1] return unicode(fh.readline().strip().rsplit('/', 1)[-1])
except IOError: except IOError:
pass pass
return branch
def _project_base(self):
if self.config:
return os.path.dirname(os.path.dirname(self.config))
return None 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) path = os.path.realpath(path)
if os.path.isfile(path): if os.path.isfile(path):
path = os.path.split(path)[0] path = os.path.split(path)[0]
@ -62,34 +59,4 @@ class Git(BaseProject):
split_path = os.path.split(path) split_path = os.path.split(path)
if split_path[1] == '': if split_path[1] == '':
return None return None
return self._find_config(split_path[0]) return self._find_git_config_file(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

View file

@ -1,20 +1,16 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
wakatime.projects.projectmap wakatime.projects.projectmap
~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Information from ~/.waka-projectmap mapping folders (relative to home folder) Use the ~/.wakatime.cfg file to define custom projects for folders.
to project names
:author: 3onyc :copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
import logging import logging
import os import os
from functools import partial
from ..packages import simplejson as json
from .base import BaseProject from .base import BaseProject
@ -23,34 +19,36 @@ log = logging.getLogger(__name__)
class ProjectMap(BaseProject): class ProjectMap(BaseProject):
def process(self): def process(self):
if not self.settings: if not self._configs:
return False return False
self.project = self._find_project() self.project = self._find_project(self.path)
return self.project != None
def _find_project(self): return self.project is not None
has_option = partial(self.settings.has_option, 'projectmap')
get_option = partial(self.settings.get, 'projectmap')
paths = self._path_generator()
projects = map(get_option, filter(has_option, paths)) def _find_project(self, path):
return projects[0] if projects else None 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): def branch(self):
return None return None
def name(self): def name(self):
return self.project if self.project:
return unicode(self.project)
def _path_generator(self): return None
"""
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)

View file

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

View file

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