diff --git a/tests/samples/output/configs_test_config_file_not_passed_in_command_line_args b/tests/samples/output/configs_test_config_file_not_passed_in_command_line_args index 71260f0..6ac5453 100644 --- a/tests/samples/output/configs_test_config_file_not_passed_in_command_line_args +++ b/tests/samples/output/configs_test_config_file_not_passed_in_command_line_args @@ -3,8 +3,8 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN] [--entity-type ENTITY_TYPE] [--category CATEGORY] [--proxy PROXY] [--no-ssl-verify] [--project PROJECT] [--alternate-project ALTERNATE_PROJECT] [--language LANGUAGE] - [--hostname HOSTNAME] [--disable-offline] [--hide-file-names] - [--hide-project-names] [--exclude EXCLUDE] + [--local-file FILE] [--hostname HOSTNAME] [--disable-offline] + [--hide-file-names] [--hide-project-names] [--exclude EXCLUDE] [--exclude-unknown-project] [--include INCLUDE] [--include-only-with-project-file] [--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL] [--timeout TIMEOUT] diff --git a/tests/samples/output/configs_test_missing_config_file b/tests/samples/output/configs_test_missing_config_file index 71260f0..6ac5453 100644 --- a/tests/samples/output/configs_test_missing_config_file +++ b/tests/samples/output/configs_test_missing_config_file @@ -3,8 +3,8 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN] [--entity-type ENTITY_TYPE] [--category CATEGORY] [--proxy PROXY] [--no-ssl-verify] [--project PROJECT] [--alternate-project ALTERNATE_PROJECT] [--language LANGUAGE] - [--hostname HOSTNAME] [--disable-offline] [--hide-file-names] - [--hide-project-names] [--exclude EXCLUDE] + [--local-file FILE] [--hostname HOSTNAME] [--disable-offline] + [--hide-file-names] [--hide-project-names] [--exclude EXCLUDE] [--exclude-unknown-project] [--include INCLUDE] [--include-only-with-project-file] [--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL] [--timeout TIMEOUT] diff --git a/tests/samples/output/main_test_timeout_passed_via_command_line b/tests/samples/output/main_test_timeout_passed_via_command_line index 1011bf5..463a116 100644 --- a/tests/samples/output/main_test_timeout_passed_via_command_line +++ b/tests/samples/output/main_test_timeout_passed_via_command_line @@ -3,8 +3,8 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN] [--entity-type ENTITY_TYPE] [--category CATEGORY] [--proxy PROXY] [--no-ssl-verify] [--project PROJECT] [--alternate-project ALTERNATE_PROJECT] [--language LANGUAGE] - [--hostname HOSTNAME] [--disable-offline] [--hide-file-names] - [--hide-project-names] [--exclude EXCLUDE] + [--local-file FILE] [--hostname HOSTNAME] [--disable-offline] + [--hide-file-names] [--hide-project-names] [--exclude EXCLUDE] [--exclude-unknown-project] [--include INCLUDE] [--include-only-with-project-file] [--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL] [--timeout TIMEOUT] diff --git a/tests/samples/output/test_help_contents b/tests/samples/output/test_help_contents index 428510e..841e410 100644 --- a/tests/samples/output/test_help_contents +++ b/tests/samples/output/test_help_contents @@ -3,8 +3,8 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN] [--entity-type ENTITY_TYPE] [--category CATEGORY] [--proxy PROXY] [--no-ssl-verify] [--project PROJECT] [--alternate-project ALTERNATE_PROJECT] [--language LANGUAGE] - [--hostname HOSTNAME] [--disable-offline] [--hide-file-names] - [--hide-project-names] [--exclude EXCLUDE] + [--local-file FILE] [--hostname HOSTNAME] [--disable-offline] + [--hide-file-names] [--hide-project-names] [--exclude EXCLUDE] [--exclude-unknown-project] [--include INCLUDE] [--include-only-with-project-file] [--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL] [--timeout TIMEOUT] @@ -46,6 +46,10 @@ optional arguments: project takes priority. --language LANGUAGE Optional language name. If valid, takes priority over auto-detected language. + --local-file FILE Absolute path to local file for the heartbeat. When + --entity is a remote file, this local file will be + used for stats and just the value of --entity sent + with heartbeat. --hostname HOSTNAME Hostname of current machine. --disable-offline Disables offline time logging instead of queuing logged time. diff --git a/tests/test_heartbeat.py b/tests/test_heartbeat.py index f3cbe45..9d6bf0b 100644 --- a/tests/test_heartbeat.py +++ b/tests/test_heartbeat.py @@ -22,6 +22,7 @@ class HeartbeatTestCase(TestCase): include = [] plugin = None include_only_with_project_file = None + local_file = None data = { 'entity': os.path.realpath('tests/samples/codefiles/python.py'), @@ -56,6 +57,7 @@ class HeartbeatTestCase(TestCase): include = [] plugin = None include_only_with_project_file = None + local_file = None data = { 'entity': os.path.realpath('tests/samples/codefiles/python.py'), diff --git a/tests/test_languages.py b/tests/test_languages.py index 0a80da1..d0bc8f7 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -102,8 +102,9 @@ class LanguagesTestCase(utils.TestCase): with utils.mock.patch('wakatime.stats.smart_guess_lexer') as mock_guess_lexer: mock_guess_lexer.return_value = None source_file = 'tests/samples/codefiles/python.py' - result = guess_language(source_file) - mock_guess_lexer.assert_called_once_with(source_file) + local_file = None + result = guess_language(source_file, local_file) + mock_guess_lexer.assert_called_once_with(source_file, local_file) self.assertEquals(result, (None, None)) def test_guess_language_from_vim_modeline(self): @@ -112,6 +113,13 @@ class LanguagesTestCase(utils.TestCase): entity='python_without_extension', ) + def test_guess_language_when_entity_not_exist_but_local_file_exists(self): + source_file = 'tests/samples/codefiles/does_not_exist.py' + local_file = 'tests/samples/codefiles/python.py' + self.assertFalse(os.path.exists(source_file)) + result = guess_language(source_file, local_file) + self.assertEquals(result[0], 'Python') + def test_language_arg_takes_priority_over_detected_language(self): self.shared( expected_language='Java', diff --git a/wakatime/arguments.py b/wakatime/arguments.py index 6f40ff7..d57b62a 100644 --- a/wakatime/arguments.py +++ b/wakatime/arguments.py @@ -116,6 +116,12 @@ def parse_arguments(): action=StoreWithoutQuotes, help='Optional language name. If valid, takes ' + 'priority over auto-detected language.') + parser.add_argument('--local-file', dest='local_file', metavar='FILE', + action=FileAction, + help='Absolute path to local file for the ' + + 'heartbeat. When --entity is a remote file, ' + + 'this local file will be used for stats and ' + + 'just the value of --entity sent with heartbeat.') parser.add_argument('--hostname', dest='hostname', action=StoreWithoutQuotes, help='Hostname of current machine.') diff --git a/wakatime/heartbeat.py b/wakatime/heartbeat.py index 21d73a1..73c25f5 100644 --- a/wakatime/heartbeat.py +++ b/wakatime/heartbeat.py @@ -85,13 +85,16 @@ class Heartbeat(object): return if self.type == 'file': self.entity = format_file_path(self.entity) - if not self.entity or not os.path.isfile(self.entity): + if not self._file_exists(): self.skip = u('File does not exist; ignoring this heartbeat.') return if self._excluded_by_missing_project_file(): self.skip = u('Skipping because missing .wakatime-project file in parent path.') return + if args.local_file and not os.path.isfile(args.local_file): + args.local_file = None + project, branch = get_project_info(configs, self, data) self.project = project self.branch = branch @@ -106,7 +109,8 @@ class Heartbeat(object): lineno=data.get('lineno'), cursorpos=data.get('cursorpos'), plugin=args.plugin, - language=data.get('language')) + language=data.get('language'), + local_file=args.local_file) except SkipHeartbeat as ex: self.skip = u(ex) or 'Skipping' return @@ -228,6 +232,10 @@ class Heartbeat(object): return None return [self._unicode(value) for value in values] + def _file_exists(self): + return (self.entity and os.path.isfile(self.entity) or + self.args.local_file and os.path.isfile(self.args.local_file)) + def _excluded_by_pattern(self): return should_exclude(self.entity, self.args.include, self.args.exclude) diff --git a/wakatime/stats.py b/wakatime/stats.py index ec67900..3fc807a 100644 --- a/wakatime/stats.py +++ b/wakatime/stats.py @@ -40,7 +40,7 @@ log = logging.getLogger('WakaTime') def get_file_stats(file_name, entity_type='file', lineno=None, cursorpos=None, - plugin=None, language=None): + plugin=None, language=None, local_file=None): if entity_type != 'file': stats = { 'language': None, @@ -52,24 +52,24 @@ def get_file_stats(file_name, entity_type='file', lineno=None, cursorpos=None, else: language, lexer = standardize_language(language, plugin) if not language: - language, lexer = guess_language(file_name) + language, lexer = guess_language(file_name, local_file) language = use_root_language(language, lexer) - parser = DependencyParser(file_name, lexer) + parser = DependencyParser(local_file or file_name, lexer) dependencies = parser.parse() stats = { 'language': language, 'dependencies': dependencies, - 'lines': number_lines_in_file(file_name), + 'lines': number_lines_in_file(local_file or file_name), 'lineno': lineno, 'cursorpos': cursorpos, } return stats -def guess_language(file_name): +def guess_language(file_name, local_file): """Guess lexer and language for a file. Returns a tuple of (language_str, lexer_obj). @@ -81,14 +81,14 @@ def guess_language(file_name): if language: lexer = get_lexer(language) else: - lexer = smart_guess_lexer(file_name) + lexer = smart_guess_lexer(file_name, local_file) if lexer: language = u(lexer.name) return language, lexer -def smart_guess_lexer(file_name): +def smart_guess_lexer(file_name, local_file): """Guess Pygments lexer for a file. Looks for a vim modeline in file contents, then compares the accuracy @@ -99,7 +99,7 @@ def smart_guess_lexer(file_name): text = get_file_head(file_name) - lexer1, accuracy1 = guess_lexer_using_filename(file_name, text) + lexer1, accuracy1 = guess_lexer_using_filename(local_file or file_name, text) lexer2, accuracy2 = guess_lexer_using_modeline(text) if lexer1: