#!python.exe
import pickle, re
from os import path
__author__ = 'Charles Grunwald (Juntalis) <cgrunwald@gmail.com>'
def __setup_configobj__(deps, info):
""" Setup configobj either in the lib folder or the local folder """
from os import unlink, mkdir
import shutil
# Download and unzip
tmpfile = deps.download(info['url'], info['filename'])
tmpfolder = path.splitext(tmpfile)[0]
deps.unzip(tmpfile, tmpfolder)
unlink(tmpfile)
modfolder = path.join(tmpfolder, 'configobj-4.7.2')
modpath = [path.join(modfolder, 'configobj.py'), path.join(modfolder, 'validate.py')]
(modpath, modconfigobj) = deps.install(modpath, 'configobj')
shutil.rmtree(tmpfolder, True)
return (modconfigobj, modpath)
""" Our dependencies """
__dependencies__ = {
'configobj':
(__setup_configobj__, {
'filename' : 'configobj.zip',
'url' : 'http://www.voidspace.org.uk/downloads/configobj-4.7.2.zip'
})
}
class ScriptDeps:
""" Simple class to install any script dependencies we don't have at startup. """
__modules__ = {}
def __init__(self, dependencies):
deps = dependencies
for dep in deps.keys():
(setup, info) = deps[dep]
self.__modules__[dep] = setup(self, info)
def get(self, name):
if self.__modules__.has_key(name):
return self.__modules__[name]
return None
def unzip(self, filename, dir):
""" Extract zip file: filename to folder: dir. """
import zipfile, os
from cStringIO import StringIO
zf = zipfile.ZipFile( filename )
namelist = zf.namelist()
dirlist = filter( lambda x: x.endswith( '/' ), namelist )
filelist = filter( lambda x: not x.endswith( '/' ), namelist )
# make base
pushd = os.getcwd()
if not path.isdir( dir ):
os.mkdir( dir )
os.chdir( dir )
# create directory structure
dirlist.sort()
for dirs in dirlist:
dirs = dirs.split( '/' )
prefix = ''
for dir in dirs:
dirname = path.join( prefix, dir )
if dir and not path.isdir( dirname ):
os.mkdir( dirname )
prefix = dirname
# extract files
for fn in filelist:
try:
out = open( fn, 'wb' )
buffer = StringIO( zf.read( fn ))
buflen = 2 ** 20
datum = buffer.read( buflen )
while datum:
out.write( datum )
datum = buffer.read( buflen )
out.close()
except:
import sys
sys.stderr.write('Error while unzipping %s..\n' % filename)
os.chdir( pushd )
def download(self, url, filename):
""" Download dependencies specified by dictionary __dependencies__"""
from urllib import urlretrieve as download
from tempfile import gettempdir as tempdir
# Iterate through dependencies, downloading and moving.
result = path.join(tempdir(), filename)
download(url, result)
return result
def _install_global(self, modpath, modname):
# install to the lib folder under Notepad++'s plugin directory.
import shutil
libdir = path.join(notepad.getNppDir(), 'plugins', 'PythonScript', 'lib')
modconfigobj = None
try:
# install to pythonscript lib folder.
for modfile in modpath:
shutil.move(modfile, libdir)
except:
(modpath, modconfigobj) = self._install_user(modpath, modname)
else:
modconfigobj = __import__(modname)
modpath = path.join(libdir, '%s.py' % modname)
return (modpath, modconfigobj)
def _install_user(self, modpath, modname):
# install to User's AppData PythonScript/lib
import shutil
libdir = path.join(path.abspath(path.dirname(path.dirname(__file__))), 'lib')
if not path.exists(libdir) or not path.isdir(libdir):
mkdir(libdir)
for modfile in modpath:
shutil.move(modfile, libdir)
modconfigobj = __import__(modname)
modpath = path.join(libdir, '%s.py' % modname)
return (modpath, modconfigobj)
def _install_stub(self, libdir):
initfile = path.join(libdir, '__init__.py')
if not path.exists(initfile):
f = open(initfile,'w')
f.write('# Stub')
f.close()
return initfile
def install(self, modpath, modname, install_global=False):
if install_global:
return self._install_global(modpath, modname)
return self._install_user(modpath, modname)
class SimpleCache(dict):
"""
Simple local cache.
It saves local data in singleton dictionary with convenient interface
Downloaded from http://code.activestate.com/recipes/577492-simple-local-cache-and-cache-decorator/
Author: Andrey Nikishaev
License: GPL
Copyright 2010, http://creotiv.in.ua
"""
def __new__(cls,*args):
if not hasattr(cls,'_instance'):
cls._instance = dict.__new__(cls)
else:
raise Exception('SimpleCache already initialized')
return cls._instance
@classmethod
def getInstance(cls):
if not hasattr(cls,'_instance'):
cls._instance = dict.__new__(cls)
return cls._instance
def get(self,name,default=None):
"""Multilevel get function.
Code:
Config().get('opt.opt_level2.key','default_value')
"""
if not name:
return default
levels = name.split('.')
data = self
for level in levels:
try:
data = data[level]
except:
return default
return data
def set(self,name,value):
"""Multilevel set function
Code:
Config().set('opt.opt_level2.key','default_value')
"""
levels = name.split('.')
arr = self
for name in levels[:-1]:
if not arr.has_key(name):
arr[name] = {}
arr = arr[name]
arr[levels[-1]] = value
def getset(self,name,value):
"""Get cache, if not exists set it and return set value
Code:
Config().getset('opt.opt_level2.key','default_value')
"""
g = self.get(name)
if not g:
g = value
self.set(name,g)
return g
def scache(func):
def wrapper(*args, **kwargs):
cache = SimpleCache.getInstance()
fn = "scache." + func.__module__ + func.__class__.__name__ + \
func.__name__ + str(args) + str(kwargs)
val = cache.get(fn)
if not val:
res = func(*args, **kwargs)
cache.set(fn,res)
return res
return val
return wrapper
# Try to import configobj module. If we cant, download it and set it up.
try:
import configobj
has_configobj = True
except ImportError:
notepad.messageBox('Could not find module "configobj". Downloading and setting it up now..', 'Dependencies')
deps = ScriptDeps(__dependencies__)
(configobj, configobj_path) = deps.get('configobj')
has_configobj = configobj is not None
if has_configobj:
notepad.messageBox('Module "configobj" setup successfully. You can find it at:\n\n%s' % configobj_path, 'Setup Successful')
else:
notepad.messageBox('Error: Could not import configobj.\nDownload at: http://www.voidspace.org.uk/python/configobj.html', 'Import Error')
exit()
class LanguageAutoDetector:
""" Main class """
__log = None
__config = None
__config_path = None
__cache = None
__detections = [
'filename',
'partial_filename',
'xml',
'shebang',
'try_shebang',
'contains_string'
#'regex'
]
__cache_ignore = {
'contains_string' : None,
'try_shebang' : 'shebang',
'partial_filename' : 'filename',
'regex' : None
}
def __init__(self, config_file=None):
config = self.__load_config(config_file)
if config['cache']['enabled']:
self.__load_cache()
#self.__log = logger.FileLogger()
def __new__(cls,*args):
if not hasattr(cls,'_instance'):
cls._instance = dict.__init__(cls, args)
else:
raise Exception('LanguageAutoDetector already initialized')
return cls._instance
@classmethod
def getInstance(cls):
if not hasattr(cls,'_instance'):
cls._instance = dict.__new__(cls)
return cls._instance
@scache
def config(self, key=None, default=None):
if self.__config is None:
config = self.__load_config()
else:
config = self.__config
if key is None:
return config
return self.__getdict(config, key, default)
@scache
def cache(self, key=None, default=None):
if self.__cache is None:
cache = self.__load_cache()
else:
cache = self.__cache
if key is None:
return cache
return self.__getdict(cache, key, default)
def set_lang(self, result, bufferID):
ret = False
lang = self.__test_language(result)
if lang is None:
if self.config('errors.invalid_lang'):
notepad.messageBox('Error: Specified language %s invalid.' % result, 'Config Error')
else:
notepad.setLangType(lang, bufferID)
ret = True
return ret
def detect(self, args):
detections = self.config('detections.order')
bufferID = args['bufferID']
args['filename'] = path.basename(notepad.getBufferFilename(bufferID))
notepad.activateBufferID(bufferID)
for detection in detections:
func = getattr(self, 'detection_%s' % detection.lower())
result = func(args)
console.write('Detection %s: %s\n' % (detection.lower(),result))
if result is not None:
if self.set_lang(result, bufferID):
break
# filename
# partial_filename
# xml
# shebang
# try_shebang
# contains_string
def detection_filename(self, args):
filename = args['filename']
config = self.config('detections.filename')
for lang in config.keys():
if filename in config[lang]:
return lang
return None
@scache
def detection_partial_filename(self, args):
filename = args['filename']
config = self.config('detections.partial_filename')
for lang in config.keys():
for pattern in config[lang]:
rgx = re.compile("^%s$" % pattern, re.IGNORECASE)
if rgx.match(filename):
# TODO: Cache here filename -> lang
return lang
return None
def detection_xml(self, args):
# Check for xml stuff.
xml_config = self.config('detections.xml')
xml_filename = args['filename']
self.xml_lang = None
def check(contents, lineNumber, totalLines):
val = contents.strip().lower()
if len(val) == 0:
return 1
else:
for pattern in xml_config:
pattern = pattern.lower()
if val.startswith(pattern):
self.xml_lang = 'xml'
ext = path.splitext(xml_filename)[1]
if len(ext) > 0:
xml_cache = { ext : 'xml' } # TODO: Cache here
return totalLines - lineNumber
editor.forEachLine(check)
return self.xml_lang
def detection_shebang(self, args):
shebang = self.__getshebang()
if shebang is None:
return None
config = self.config('detections.shebang')
for lang in config.keys():
for pattern in config[lang]:
rgx = re.compile(r"^#!((?:/[^\s]+/env(?:\.[a-z]+)? |/[^\s]+/|[A-Z]:[^\s]*\\|[A-Z]:[^\s]*\\env\.[a-z]+ ?)?%s[\d.-_]*(?:\.[0-9a-z-_.])*)\b" % pattern, re.IGNORECASE)
if rgx.search(shebang):
# TODO: Cache here shebang -> lang
return lang
return None
def detection_try_shebang(self, args):
shebang = self.__getshebang()
if shebang is None:
return None
rgx = re.compile(r"^#!(?:/[^\s]+/env(?:\.[a-z]+)? |/[^\s]+/|[A-Z]:[^\s]*\\|[A-Z]:[^\s]*\\env\.[a-z]+ ?)?([^\s]+)[\d.-_]*(?:\.[0-9a-z-_.])*\b", re.IGNORECASE)
match = rgx.search(shebang)
if match:
result = match.group(1)
lang = self.__test_language(result)
if lang is None:
return None
return result
return None
def detection_contains_string(self, args):
text = editor.getText()
config = self.config('detections.contains_string')
for lang in config.keys():
for pattern in config[lang]:
rgx = re.compile("%s" % pattern, re.IGNORECASE | re.MULTILINE)
if rgx.search(text):
# TODO: Cache here shebang -> lang
return lang
return None
def detection_regex(self, args):
pass
def __getshebang(self):
line = editor.getLine(0)
if len(line) <= 2:
return None
if line[0:2] == '#!':
return line
return None
def __getdict(self, dict, name, default=None):
"""Multilevel get function.
Code:
Config().get('opt.opt_level2.key','default_value')
"""
if not name:
return default
levels = name.split('.')
data = dict
for level in levels:
try:
data = data[level]
except:
return default
return data
def __load_config(self, cfg=None):
""" Load configuration for script. If it doesn't exist, write the default
configuration to file. """
# Figure out config path.
if cfg is None:
cfg = path.join(notepad.getPluginConfigDir(), 'py_autolang.cfg')
self.__config_path = cfg
if path.exists(cfg):
self.__config = configobj.ConfigObj(cfg)
else:
self.__config = self.__default_config()
self.__save_config()
return self.__config
def __default_config(self, cfg=None):
""" Default configuration for script. """
# Figure out config path.
if cfg is None and self.__config_path is None:
cfg = path.join(notepad.getPluginConfigDir(), 'py_autolang.cfg')
elif cfg is None and self.__config_path is not None:
cfg = self.__config_path
config = configobj.ConfigObj()
config.filename = cfg
# Main config
config['script'] = {}
config['script']['enabled'] = True
config['script']['autoload'] = True
# Errors
config['errors'] = {}
config['errors']['invalid_lang'] = True # Message box on invalid lexer specified.
config['errors']['invalid_lexer'] = True # Message box on invalid lang specified.
# Script cache
config['cache'] = {}
config['cache']['enabled'] = True
cache_folder = path.abspath(path.join(path.dirname(cfg), 'cache'))
valid_folder = False
while not valid_folder:
if path.exists(cache_folder):
if not path.isdir(cache_folder):
import string, random
cache_folder += '-' + ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(3))
else:
valid_folder = True
else:
from os import mkdir
mkdir(cache_folder)
config['cache']['folder'] = cache_folder
# Logging
config['logging'] = {}
config['logging']['console'] = False
config['logging']['console_auto_open'] = False
config['logging']['file'] = False
# Loading event
config['file_load_event'] = {}
config['file_load_event']['no_extension'] = True
config['file_load_event']['default_lexer'] = True
config['file_load_event']['always'] = False
"""
Detection methods order - This tells what order to use detection methods.
So if filename is executed before shebang, and filename matches a detection,
the shebang line wont be run. To disable a detection method, set to 0. If
any two methods have the same number, an error will be thrown.
Possible detections:
shebang
try_shebang
xml
filename
partial_filename
contains_string
regex - Not yet implemented
"""
order = ['filename', 'partial_filename', 'xml', 'shebang', 'try_shebang', 'contains_string']
# Default Detections
## Shebangs
## These all match the follow regex pattern:
### r"^#!(?:/[^\s]+/env(?:\.[a-z]+) |/[^\s]+/|[A-Z]:[^\s]*\\|[A-Z]:[^\s]*\\env\.[a-z]+ ?)?%s[\d.-_]*(?:\.[0-9a-z-_.])*\b" % key
shebang = {
# Shell
## Bash/Korn Shell/C Shell/Z Shell/etc
'bash' : ['(?:[czk]|ba)?sh'],
# Python
## CPython - http://python.org/
## Cross Twine Linker (xtpython) - http://crosstwine.com/linker/python.html
## Unpython Python to C compiler (unpython) - http://code.google.com/p/unpython/
## IPython (ipython) - http://ipython.org/
## PyPy - http://pypy.org/
## Iron Python - http://ironpython.net/
## Mozilla Embedded Python Console - http://www.thomas-schilz.de/MozPython/
## TinyPy - http://www.tinypy.org/
## Snipy - Personal project, you can remove if you want.
## Enthought SciPy distribution - http://www.enthought.com/
## Jython (jython) - http://www.jython.org/
## Cython (Optimizing Python to C Compiler) - http://cython.org/
## Typhon (typhon) - https://github.com/vic/typhon
## Mython (mython) - http://mython.org/
# Languages Close Enough to Python """
## Nimrod - http://force7.de/nimrod/download.html
## Serpent - http://sourceforge.net/projects/serpent/
## Boo - http://boo.codehaus.org/
'python' : [
'(?:xt|un|i)?pythonw?',
'(?:py|i|moz|tiny|sni)pyw?(?:-c)?',
'epdw?',
'[jctm]ythonw?',
'(?:nimrod|ser?pent)',
'boo(?:c|i|ish)?'
],
# Perl
## Perl - http://www.perl.org/
## Parrot - http://parrot.org/
### Hm, this one might be hard. Leaving parrot as perl for now
'perl' : [ 'w?perl', 'parrot' ],
# Ruby
## Ruby - http://www.ruby-lang.org/en/
## Iron Ruby (ir, etc) - http://ironruby.net/
## JRuby - http://jruby.org/
## Ruby on Rails - http://rubyonrails.org/
'ruby' : [
'[ji]?[ie]r[wbi]{0,2}?(?:_swing)?',
'[ej]?ruby[wc]?',
'rake'
],
# Javascript
## Node.Js - http://nodejs.org/
## Narwhal - https://github.com/tlrobinson/narwhal
## JSDB - http://www.jsdb.org/
## Ringo Javascript - http://ringojs.org/
## GlueScript - http://gluescript.sourceforge.net/
## Rhino - http://www.mozilla.org/rhino/
'javascript' : [
'(?:node|npm)',
'(?:narwhal|tusk)',
'(?:jsdb|ringo|gluew?)',
'(?:rhino|js)'
],
# PHP
'php' : [
'(?:i?php(?:-cgi|-cli|-win)?|pharc?)'
]
}
filename = {
'bash' : ['configure'],
'makefile' : ['Makefile']
}
partial_filename = {
'makefile' : ['Makefile\..+']
}
contains_string = {
'bash' : ['^mk_add_options', '^ac_add_options']
}
config['detections'] = {
'order' : order,
'filename' : filename,
'partial_filename' : partial_filename,
'xml' : ['<?xml ', '<!DOCTYPE'],
'shebang' : shebang,
'contains_string' : contains_string
}
return config
def __save_config(self):
self.__config.write()
def __load_cache(self):
config = self.config()
folder = config['cache']['folder']
cache = {}
for detection in config['detections']['order']:
if detection in self.__cache_ignore:
if self.__cache_ignore[detection] is not None:
detection = self.__cache_ignore[detection]
if cache.has_key(detection):
continue
else:
continue
f = path.join(folder, detection)
if path.exists(f) and path.isfile(f):
input = open(f, 'rb')
cache[detection] = {
'changed' : False,
'value' : pickle.load(input)
}
input.close()
else:
cache[detection] = {
'changed' : False,
'value' : None
}
self.__cache = cache
return self.__cache
def __save_cache(self):
config = self.config()
folder = config['cache']['folder']
saved = []
for detection in config['detections']['order']:
if detection in self.__cache_ignore:
if self.__cache_ignore[detection] is not None:
detection = self.__cache_ignore[detection]
if detection in saved:
continue
else:
continue
f = path.join(folder, detection)
cache = self.cache(detection)
if cache['changed'] and cache['value'] is not None:
output = open(f, 'wb')
pickle.dump(cache['value'], output)
output.close()
self.__cache[detection]['changed'] = False
saved.append(detection)
def __test_lexer(self, lexer):
result = True
old_lexer = editor.getLexerLanguage()
editor.setLexerLanguage(lexer)
if editor.getLexerLanguage() == 'null':
result = False
editor.setLexerLanguage(old_lexer)
return result
def __test_language(self, lang):
import Npp
lang = lang.upper()
result = None
try:
result = getattr(Npp.LANGTYPE, lang)
except AttributeError:
result = None
return result
detector = LanguageAutoDetector()
notepad.clearCallbacks([NOTIFICATION.FILEOPENED])
notepad.callback(detector.detect, [NOTIFICATION.FILEOPENED])