From 95062747ef69d11348a3054ba6011fdf5e67129d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 12 May 2015 17:20:31 +0200 Subject: [PATCH] Extracted setuptools related things into reusable package Babel related commands and the Clean command for setup.py are not reusable by plugins as well. Also added a factory method for plugin setup parameters. --- setup.py | 261 ++----------------- src/octoprint/setuptools/__init__.py | 368 +++++++++++++++++++++++++++ 2 files changed, 392 insertions(+), 237 deletions(-) create mode 100644 src/octoprint/setuptools/__init__.py diff --git a/setup.py b/setup.py index 2a31ff8b..1da79eb9 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,14 @@ #!/usr/bin/env python # coding=utf-8 -from setuptools import setup, find_packages, Command +from setuptools import setup, find_packages import os -import shutil -import glob import versioneer +import sys +sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "src")) +import octoprint.setuptools + #----------------------------------------------------------------------------------------------------------------------- # Requirements for our application @@ -40,10 +42,7 @@ EXTRA_REQUIRES = dict( # Documentation dependencies "sphinx>=1.3", "sphinxcontrib-httpdomain", - "sphinx_rtd_theme", - - # Translation dependencies - "po2json" + "sphinx_rtd_theme" ] ) @@ -52,11 +51,9 @@ DEPENDENCY_LINKS = [] # I18N setup I18N_MAPPING_FILE = "babel.cfg" -I18N_DOMAIN = "messages" I18N_INPUT_DIRS = "." -I18N_OUTPUT_DIR_PY = os.path.join("src", "octoprint", "translations") -I18N_OUTPUT_DIR_JS = os.path.join("src", "octoprint", "static", "js", "i18n") -I18N_POT_FILE = os.path.join(I18N_OUTPUT_DIR_PY, "messages.pot") +I18N_OUTPUT_DIR = os.path.join("src", "octoprint", "translations") +I18N_POT_FILE = os.path.join(I18N_OUTPUT_DIR, "messages.pot") # Versioneer configuration versioneer.VCS = 'git' @@ -69,232 +66,17 @@ versioneer.lookupfile = '.versioneer-lookup' #----------------------------------------------------------------------------------------------------------------------- # Anything below here is just command setup and general setup configuration -def package_data_dirs(source, sub_folders): - dirs = [] - - for d in sub_folders: - for dirname, _, files in os.walk(os.path.join(source, d)): - dirname = os.path.relpath(dirname, source) - for f in files: - dirs.append(os.path.join(dirname, f)) - - return dirs - - -def _recursively_handle_files(directory, file_matcher, folder_handler=None, file_handler=None): - applied_handler = False - - for filename in os.listdir(directory): - path = os.path.join(directory, filename) - - if file_handler is not None and file_matcher(filename): - file_handler(path) - applied_handler = True - - elif os.path.isdir(path): - sub_applied_handler = _recursively_handle_files(path, file_matcher, folder_handler=folder_handler, file_handler=file_handler) - if sub_applied_handler: - applied_handler = True - - if folder_handler is not None: - folder_handler(path, sub_applied_handler) - - return applied_handler - -class CleanCommand(Command): - description = "clean build artifacts" - user_options = [] - boolean_options = [] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - # build folder - if os.path.exists('build'): - print "Deleting build directory" - shutil.rmtree('build') - - # eggs - eggs = glob.glob('OctoPrint*.egg-info') - for egg in eggs: - print "Deleting %s directory" % egg - shutil.rmtree(egg) - - # pyc files - def delete_folder_if_empty(path, applied_handler): - if not applied_handler: - return - if len(os.listdir(path)) == 0: - shutil.rmtree(path) - print "Deleted %s since it was empty" % path - - def delete_file(path): - os.remove(path) - print "Deleted %s" % path - - import fnmatch - _recursively_handle_files( - os.path.abspath("src"), - lambda name: fnmatch.fnmatch(name.lower(), "*.pyc"), - folder_handler=delete_folder_if_empty, - file_handler=delete_file - ) - - # pyc files - def delete_folder_if_empty(path, applied_handler): - if not applied_handler: - return - if len(os.listdir(path)) == 0: - shutil.rmtree(path) - print "Deleted %s since it was empty" % path - - def delete_file(path): - os.remove(path) - print "Deleted %s" % path - - import fnmatch - _recursively_handle_files( - os.path.abspath("src"), - lambda name: fnmatch.fnmatch(name.lower(), "*.pyc"), - folder_handler=delete_folder_if_empty, - file_handler=delete_file - ) - - -class NewTranslation(Command): - description = "create a new translation" - user_options = [ - ('locale=', 'l', 'locale for the new translation'), - ] - boolean_options = [] - - def __init__(self, dist, **kw): - from babel.messages import frontend as babel - self.babel_init_messages = babel.init_catalog(dist) - Command.__init__(self, dist, **kw) - - def initialize_options(self): - self.locale = None - self.babel_init_messages.initialize_options() - - def finalize_options(self): - self.babel_init_messages.locale = self.locale - self.babel_init_messages.input_file = I18N_POT_FILE - self.babel_init_messages.output_dir = I18N_OUTPUT_DIR_PY - self.babel_init_messages.finalize_options() - - def run(self): - self.babel_init_messages.run() - - -class ExtractTranslation(Command): - description = "extract translations" - user_options = [] - boolean_options = [] - - def __init__(self, dist, **kw): - from babel.messages import frontend as babel - self.babel_extract_messages = babel.extract_messages(dist) - Command.__init__(self, dist, **kw) - - def initialize_options(self): - self.babel_extract_messages.initialize_options() - - def finalize_options(self): - self.babel_extract_messages.mapping_file = I18N_MAPPING_FILE - self.babel_extract_messages.output_file = I18N_POT_FILE - self.babel_extract_messages.input_dirs = I18N_INPUT_DIRS - self.babel_extract_messages.msgid_bugs_address = "i18n@octoprint.org" - self.babel_extract_messages.copyright_holder = "The OctoPrint Project" - self.babel_extract_messages.finalize_options() - - def run(self): - self.babel_extract_messages.run() - - -class RefreshTranslation(Command): - description = "refresh translations" - user_options = [ - ('locale=', 'l', 'locale for the translation to refresh'), - ] - boolean_options = [] - - def __init__(self, dist, **kw): - from babel.messages import frontend as babel - self.babel_extract_messages = babel.extract_messages(dist) - self.babel_update_messages = babel.update_catalog(dist) - Command.__init__(self, dist, **kw) - - def initialize_options(self): - self.locale = None - self.babel_extract_messages.initialize_options() - self.babel_update_messages.initialize_options() - - def finalize_options(self): - self.babel_extract_messages.mapping_file = I18N_MAPPING_FILE - self.babel_extract_messages.output_file = I18N_POT_FILE - self.babel_extract_messages.input_dirs = I18N_INPUT_DIRS - self.babel_extract_messages.msgid_bugs_address = "i18n@octoprint.org" - self.babel_extract_messages.copyright_holder = "The OctoPrint Project" - self.babel_extract_messages.finalize_options() - - self.babel_update_messages.input_file = I18N_POT_FILE - self.babel_update_messages.output_dir = I18N_OUTPUT_DIR_PY - self.babel_update_messages.locale = self.locale - - def run(self): - self.babel_extract_messages.run() - self.babel_update_messages.run() - - -class CompileTranslation(Command): - description = "compile translations" - user_options = [] - boolean_options = [] - - def __init__(self, dist, **kw): - from babel.messages import frontend as babel - self.babel_compile_messages = babel.compile_catalog(dist) - Command.__init__(self, dist, **kw) - - def initialize_options(self): - self.babel_compile_messages.initialize_options() - - def finalize_options(self): - self.babel_compile_messages.directory = I18N_OUTPUT_DIR_PY - - def run(self): - self.babel_compile_messages.run() - - import po2json - - for lang_code in os.listdir(I18N_OUTPUT_DIR_PY): - full_path = os.path.join(I18N_OUTPUT_DIR_PY, lang_code) - - if os.path.isdir(full_path): - client_po_dir = os.path.join(full_path, "LC_MESSAGES") - - po2json.update_js_file( - "%s/%s.po" % (client_po_dir, I18N_DOMAIN), - lang_code, - I18N_OUTPUT_DIR_JS, - I18N_DOMAIN - ) - - def get_cmdclass(): cmdclass = versioneer.get_cmdclass() - cmdclass.update({ - 'clean': CleanCommand, - 'babel_new': NewTranslation, - 'babel_extract': ExtractTranslation, - 'babel_refresh': RefreshTranslation, - 'babel_compile': CompileTranslation - }) + + # add clean command + cmdclass.update(dict(clean=octoprint.setuptools.CleanCommand.for_options(source_folder="src", eggs=["OctoPrint*.egg-info"]))) + + # add translation commands + translation_dir = os.path.join("src", "octoprint", "translations") + pot_file = os.path.join(translation_dir, "messages.pot") + cmdclass.update(octoprint.setuptools.get_babel_commandclasses(pot_file=pot_file, output_dir=translation_dir)) + return cmdclass @@ -330,8 +112,13 @@ def params(): license = "AGPLv3" packages = find_packages(where="src") - package_dir = {"octoprint": "src/octoprint"} - package_data = {"octoprint": package_data_dirs('src/octoprint', ['static', 'templates', 'plugins', 'translations'])} + package_dir = { + "octoprint": "src/octoprint", + "octoprint_setuptools": "src/octoprint_setuptools" + } + package_data = { + "octoprint": octoprint.setuptools.package_data_dirs('src/octoprint', ['static', 'templates', 'plugins', 'translations']) + } include_package_data = True zip_safe = False diff --git a/src/octoprint/setuptools/__init__.py b/src/octoprint/setuptools/__init__.py new file mode 100644 index 00000000..771e1c35 --- /dev/null +++ b/src/octoprint/setuptools/__init__.py @@ -0,0 +1,368 @@ +# coding=utf-8 +from __future__ import absolute_import + +__author__ = "Gina Häußge " +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' +__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License" + +import os +import shutil +import glob + +from setuptools import Command + + +def package_data_dirs(source, sub_folders): + import os + dirs = [] + + for d in sub_folders: + folder = os.path.join(source, d) + if not os.path.exists(folder): + continue + + for dirname, _, files in os.walk(folder): + dirname = os.path.relpath(dirname, source) + for f in files: + dirs.append(os.path.join(dirname, f)) + + return dirs + + +def recursively_handle_files(directory, file_matcher, folder_handler=None, file_handler=None): + applied_handler = False + + for filename in os.listdir(directory): + path = os.path.join(directory, filename) + + if file_handler is not None and file_matcher(filename): + file_handler(path) + applied_handler = True + + elif os.path.isdir(path): + sub_applied_handler = recursively_handle_files(path, file_matcher, folder_handler=folder_handler, file_handler=file_handler) + if sub_applied_handler: + applied_handler = True + + if folder_handler is not None: + folder_handler(path, sub_applied_handler) + + return applied_handler + + +class CleanCommand(Command): + description = "clean build artifacts" + user_options = [] + boolean_options = [] + + build_folder = "build" + source_folder = "src" + eggs = [] + + @classmethod + def for_options(cls, build_folder="build", source_folder="src", eggs=None): + if eggs is None: + eggs = [] + return type(cls)(cls.__name__, (cls,), dict( + build_folder=build_folder, + source_folder=source_folder, + eggs=eggs + )) + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + # build folder + if os.path.exists(self.__class__.build_folder): + print "Deleting build directory" + shutil.rmtree(self.__class__.build_folder) + + # eggs + for egg in self.__class__.eggs: + globbed_eggs = glob.glob(egg) + for globbed_egg in globbed_eggs: + print "Deleting %s directory" % globbed_egg + shutil.rmtree(globbed_egg) + + # pyc files + def delete_folder_if_empty(path, applied_handler): + if not applied_handler: + return + if len(os.listdir(path)) == 0: + shutil.rmtree(path) + print "Deleted %s since it was empty" % path + + def delete_file(path): + os.remove(path) + print "Deleted %s" % path + + import fnmatch + recursively_handle_files( + os.path.abspath(self.__class__.source_folder), + lambda name: fnmatch.fnmatch(name.lower(), "*.pyc"), + folder_handler=delete_folder_if_empty, + file_handler=delete_file + ) + + +class NewTranslation(Command): + description = "create a new translation" + user_options = [ + ('locale=', 'l', 'locale for the new translation'), + ] + boolean_options = [] + + pot_file = None + output_dir = None + + @classmethod + def for_options(cls, pot_file=None, output_dir=None): + if pot_file is None: + raise ValueError("pot_file must not be None") + if output_dir is None: + raise ValueError("output_dir must not be None") + + return type(cls)(cls.__name__, (cls,), dict( + pot_file=pot_file, + output_dir=output_dir + )) + + def __init__(self, dist, **kw): + from babel.messages import frontend as babel + self.babel_init_messages = babel.init_catalog(dist) + Command.__init__(self, dist, **kw) + + def initialize_options(self): + self.locale = None + self.babel_init_messages.initialize_options() + + def finalize_options(self): + self.babel_init_messages.locale = self.locale + self.babel_init_messages.input_file = self.__class__.pot_file + self.babel_init_messages.output_dir = self.__class__.output_dir + self.babel_init_messages.finalize_options() + + def run(self): + self.babel_init_messages.run() + + +class ExtractTranslation(Command): + description = "extract translations" + user_options = [] + boolean_options = [] + + mail_address = "i18n@octoprint.org" + copyright_holder = "The OctoPrint Project" + mapping_file = None + pot_file = None + input_dirs = None + + @classmethod + def for_options(cls, mail_address="i18n@octoprint.org", copyright_holder="The OctoPrint Project", mapping_file=None, pot_file=None, input_dirs=None): + if mapping_file is None: + raise ValueError("mapping_file must not be None") + if pot_file is None: + raise ValueError("pot_file must not be None") + if input_dirs is None: + raise ValueError("input_dirs must not be None") + + return type(cls)(cls.__name__, (cls,), dict( + mapping_file=mapping_file, + pot_file=pot_file, + input_dirs=input_dirs, + mail_address=mail_address, + copyright_holder=copyright_holder + )) + + def __init__(self, dist, **kw): + from babel.messages import frontend as babel + self.babel_extract_messages = babel.extract_messages(dist) + Command.__init__(self, dist, **kw) + + def initialize_options(self): + self.babel_extract_messages.initialize_options() + + def finalize_options(self): + self.babel_extract_messages.mapping_file = self.__class__.mapping_file + self.babel_extract_messages.output_file = self.__class__.pot_file + self.babel_extract_messages.input_dirs = self.__class__.input_dirs + self.babel_extract_messages.msgid_bugs_address = self.__class__.mail_address + self.babel_extract_messages.copyright_holder = self.__class__.copyright_holder + self.babel_extract_messages.finalize_options() + + def run(self): + self.babel_extract_messages.run() + + +class RefreshTranslation(Command): + description = "refresh translations" + user_options = [ + ('locale=', 'l', 'locale for the translation to refresh'), + ] + boolean_options = [] + + mail_address = "i18n@octoprint.org" + copyright_holder = "The OctoPrint Project" + mapping_file = None + pot_file = None + input_dirs = None + output_dir = None + + @classmethod + def for_options(cls, mail_address="i18n@octoprint.org", copyright_holder="The OctoPrint Project", mapping_file=None, pot_file=None, input_dirs=None, output_dir=None): + if mapping_file is None: + raise ValueError("mapping_file must not be None") + if pot_file is None: + raise ValueError("pot_file must not be None") + if input_dirs is None: + raise ValueError("input_dirs must not be None") + if output_dir is None: + raise ValueError("output_dir must not be None") + + return type(cls)(cls.__name__, (cls,), dict( + mapping_file=mapping_file, + pot_file=pot_file, + input_dirs=input_dirs, + mail_address=mail_address, + copyright_holder=copyright_holder, + output_dir=output_dir + )) + + def __init__(self, dist, **kw): + from babel.messages import frontend as babel + self.babel_extract_messages = babel.extract_messages(dist) + self.babel_update_messages = babel.update_catalog(dist) + Command.__init__(self, dist, **kw) + + def initialize_options(self): + self.locale = None + self.babel_extract_messages.initialize_options() + self.babel_update_messages.initialize_options() + + def finalize_options(self): + self.babel_extract_messages.mapping_file = self.__class__.mapping_file + self.babel_extract_messages.output_file = self.__class__.pot_file + self.babel_extract_messages.input_dirs = self.__class__.input_dirs + self.babel_extract_messages.msgid_bugs_address = self.__class__.mail_address + self.babel_extract_messages.copyright_holder = self.__class__.copyright_holder + self.babel_extract_messages.finalize_options() + + self.babel_update_messages.input_file = self.__class__.pot_file + self.babel_update_messages.output_dir = self.__class__.output_dir + self.babel_update_messages.locale = self.locale + + def run(self): + self.babel_extract_messages.run() + self.babel_update_messages.run() + + +class CompileTranslation(Command): + description = "compile translations" + user_options = [] + boolean_options = [] + + output_dir = None + + @classmethod + def for_options(cls, output_dir=None): + if output_dir is None: + raise ValueError("output_dir must not be None") + + return type(cls)(cls.__name__, (cls,), dict( + output_dir=output_dir + )) + + def __init__(self, dist, **kw): + from babel.messages import frontend as babel + self.babel_compile_messages = babel.compile_catalog(dist) + Command.__init__(self, dist, **kw) + + def initialize_options(self): + self.babel_compile_messages.initialize_options() + + def finalize_options(self): + self.babel_compile_messages.directory = self.__class__.output_dir + + def run(self): + self.babel_compile_messages.run() + + +def get_babel_commandclasses(pot_file=None, mapping_file="babel.cfg", input_dirs=".", output_dir=None, mail_address="i18n@octoprint.org", copyright_holder="The OctoPrint Project"): + return dict( + babel_new=NewTranslation.for_options(pot_file=pot_file, output_dir=output_dir), + babel_extract=ExtractTranslation.for_options(mapping_file=mapping_file, pot_file=pot_file, input_dirs=input_dirs, mail_address=mail_address, copyright_holder=copyright_holder), + babel_refresh=RefreshTranslation.for_options(mapping_file=mapping_file, pot_file=pot_file, input_dirs=input_dirs, output_dir=output_dir, mail_address=mail_address, copyright_holder=copyright_holder), + babel_compile=CompileTranslation.for_options(output_dir=output_dir) + ) + + +def create_plugin_setup_parameters(identifier="todo", name="TODO", version="0.1", description="TODO", author="TODO", + mail="todo@example.com", url="TODO", license="AGPLv3", additional_data=None, + requires=None, extra_requires=None, cmdclass=None): + package = "octoprint_{identifier}".format(**locals()) + + if additional_data is None: + additional_data = list() + + if requires is None: + requires = ["OctoPrint"] + if not isinstance(requires, (list, tuple)): + raise ValueError("requires must be a list or a tuple") + if "OctoPrint" not in requires: + requires = ["OctoPrint"] + list(requires) + + if extra_requires is None: + extra_requires = dict() + if not isinstance(extra_requires, dict): + raise ValueError("extra_requires must be a dict") + + if cmdclass is None: + cmdclass = dict() + if not isinstance(cmdclass, dict): + raise ValueError("cmdclass must be a dict") + + cmdclass.update(dict( + clean=CleanCommand.for_options(source_folder=package) + )) + + translation_dir = os.path.join(package, "translations") + pot_file = os.path.join(translation_dir, "messages.pot") + if os.path.isdir(translation_dir) and os.path.isfile(pot_file): + cmdclass.update(get_babel_commandclasses(pot_file=pot_file, output_dir=translation_dir)) + + return dict( + name=name, + version=version, + description=description, + author=author, + author_email=mail, + url=url, + license=license, + + # adding new commands + cmdclass=cmdclass, + + # we only have our plugin package to install + packages=[package], + + # we might have additional data files in sub folders that need to be installed too + package_data={package: package_data_dirs(package, ["static", "templates", "translations"] + additional_data)}, + include_package_data=True, + + # If you have any package data that needs to be accessible on the file system, such as templates or static assets + # this plugin is not zip_safe. + zip_safe=False, + + install_requires=requires, + extras_require=extra_requires, + + # Hook the plugin into the "octoprint.plugin" entry point, mapping the plugin_identifier to the plugin_package. + # That way OctoPrint will be able to find the plugin and load it. + entry_points={ + "octoprint.plugin": ["{identifier} = {package}".format(**locals())] + } + )