├── .envrc ├── .gitignore ├── .travis.yml ├── Makefile ├── README.md ├── python_fu ├── __init__.py ├── commandline.py ├── commands │ ├── __init__.py │ ├── demote.py │ ├── lsmodules.py │ ├── mkmodule.py │ └── promote.py ├── compat.py ├── helpers.py └── module.py ├── setup.py ├── tests ├── __init__.py ├── helpers.py ├── test_create.py ├── test_demote.py ├── test_module.py └── test_promote.py └── tox.ini /.envrc: -------------------------------------------------------------------------------- 1 | layout python 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | build/ 4 | dist/ 5 | .tox/ 6 | .direnv/ 7 | *.egg-info/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.5" 5 | - "2.6" 6 | - "2.7" 7 | - "3.2" 8 | - "pypy" 9 | 10 | install: 11 | - pip install pytest 12 | 13 | script: 14 | - py.test -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | 3 | clean: 4 | rm -rf build/ dist/ python_fu.egg-info/ 5 | 6 | release: clean 7 | # Check if latest tag is the current head we're releasing 8 | echo "Latest tag = $$(git tag | sort -nr | head -n1)" 9 | echo "HEAD SHA = $$(git sha head)" 10 | echo "Latest tag SHA = $$(git tag | sort -nr | head -n1 | xargs git sha)" 11 | @test "$$(git sha head)" = "$$(git tag | sort -nr | head -n1 | xargs git sha)" 12 | make force_release 13 | 14 | force_release: clean 15 | git push --tags 16 | python setup.py sdist bdist_wheel 17 | twine upload dist/* 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python-fu: Useful shell scripts for Python devs 2 | =============================================== 3 | 4 | [![Build status](https://travis-ci.org/nvie/python-fu.png)](https://travis-ci.org/nvie/python-fu) 5 | 6 | Create dir structures for your modules easily: 7 | 8 | $ mkmodule foo.bar.qux 9 | $ tree foo 10 | foo 11 | ├── __init__.py 12 | └── bar 13 | ├── __init__.py 14 | └── qux.py 15 | 16 | Easily promote module files: 17 | 18 | $ promote foo.bar.qux 19 | $ tree foo 20 | foo 21 | ├── __init__.py 22 | └── bar 23 | ├── __init__.py 24 | └── qux 25 | └── __init__.py 26 | 27 | Easily demote modules files (if safe): 28 | 29 | $ demote foo.bar.qux 30 | $ tree foo 31 | foo 32 | ├── __init__.py 33 | └── bar 34 | ├── __init__.py 35 | └── qux.py 36 | 37 | Safety first 38 | ============ 39 | 40 | These commands will never cause any data loss. 41 | 42 | 43 | Installation 44 | ============ 45 | 46 | You can use `pip` to install python-fu: 47 | 48 | ```console 49 | $ pip install python-fu 50 | ``` 51 | -------------------------------------------------------------------------------- /python_fu/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nvie/python-fu/2c92fe499e7b641582fde5d93eca29b26499076d/python_fu/__init__.py -------------------------------------------------------------------------------- /python_fu/commandline.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import click 4 | 5 | 6 | def info(msg): 7 | click.echo(msg) 8 | 9 | 10 | def warning(msg): 11 | click.secho(msg, fg='yellow', file=sys.stderr) 12 | 13 | 14 | def error(msg): 15 | click.secho(msg, fg='red', file=sys.stderr) 16 | 17 | 18 | def exit(msg, exitcode=1): 19 | error(msg) 20 | sys.exit(exitcode) 21 | -------------------------------------------------------------------------------- /python_fu/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nvie/python-fu/2c92fe499e7b641582fde5d93eca29b26499076d/python_fu/commands/__init__.py -------------------------------------------------------------------------------- /python_fu/commands/demote.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import click 4 | from python_fu.module import Module 5 | 6 | 7 | @click.command() 8 | @click.argument('modules', nargs=-1) 9 | def cli(modules): 10 | """Demotes Python packages to modules (if safe)""" 11 | for module in [Module(m) for m in modules]: 12 | module.demote() 13 | 14 | 15 | if __name__ == '__main__': 16 | cli() 17 | -------------------------------------------------------------------------------- /python_fu/commands/lsmodules.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import ast 4 | 5 | import click 6 | from pathlib import Path 7 | 8 | from more_itertools import unique_everseen 9 | 10 | 11 | def walk_python_files(*paths): 12 | for path in paths: 13 | path = Path(path) 14 | if path.is_file() and path.suffix == '.py': 15 | yield str(path) 16 | elif path.is_dir(): 17 | for f in walk_python_files(*path.iterdir()): 18 | yield f 19 | 20 | 21 | def iter_imported_modules_from_file(python_file): 22 | with open(python_file, 'r') as f: 23 | source = f.read() 24 | tree = ast.parse(source, filename=python_file) 25 | 26 | for node in ast.walk(tree): 27 | if isinstance(node, ast.Import): 28 | for name_node in node.names: 29 | yield name_node.name 30 | elif isinstance(node, ast.ImportFrom): 31 | yield node.module 32 | 33 | 34 | def iter_imported_modules(paths): 35 | for python_file in walk_python_files(*paths): 36 | for mod in filter(None, iter_imported_modules_from_file(python_file)): 37 | yield mod 38 | 39 | 40 | @click.command() 41 | @click.argument('paths', nargs=-1) 42 | def cli(paths): 43 | """Lists all module names that are imported anywhere in the given paths""" 44 | for mod in sorted(unique_everseen(iter_imported_modules(paths))): 45 | print(mod) 46 | 47 | 48 | if __name__ == '__main__': 49 | cli() 50 | -------------------------------------------------------------------------------- /python_fu/commands/mkmodule.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import click 4 | from python_fu.module import Module 5 | 6 | 7 | @click.command() 8 | @click.option('--promote', '-p', flag=True, 9 | help='Immediately promote the new module to a package') 10 | @click.argument('modules', nargs=-1) 11 | def cli(modules, promote): 12 | """Creates directory structures for Python packages""" 13 | for module in [Module(m) for m in sorted(modules)]: 14 | if module.exists(): 15 | warning('{} already exists, skipping.'.format(module)) 16 | continue 17 | 18 | info('Creating {}'.format(module.module_file)) 19 | module.create(promote) 20 | 21 | 22 | if __name__ == '__main__': 23 | cli() 24 | -------------------------------------------------------------------------------- /python_fu/commands/promote.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import click 4 | from python_fu.module import Module 5 | 6 | 7 | @click.command() 8 | @click.argument('modules', nargs=-1) 9 | def cli(modules): 10 | """Promotes Python modules to packages""" 11 | for module in [Module(m) for m in modules]: 12 | module.promote() 13 | 14 | 15 | if __name__ == '__main__': 16 | cli() 17 | -------------------------------------------------------------------------------- /python_fu/compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | PY3 = sys.version_info[0] == 3 5 | 6 | 7 | if PY3: 8 | def u(s): 9 | return s 10 | else: 11 | def u(s): # noqa 12 | return unicode(s, 'unicode_escape') 13 | 14 | 15 | def python_2_unicode_compatible(klass): 16 | """ 17 | A decorator that defines __unicode__ and __str__ methods under Python 2. 18 | Under Python 3 it does nothing. 19 | 20 | To support Python 2 and 3 with a single code base, define a __str__ method 21 | returning text and apply this decorator to the class. 22 | """ 23 | if not PY3: 24 | klass.__unicode__ = klass.__str__ 25 | klass.__str__ = lambda self: self.__unicode__().encode('utf-8') 26 | return klass 27 | -------------------------------------------------------------------------------- /python_fu/helpers.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from .compat import u 3 | 4 | 5 | def replace_extension(filename, new_extension): 6 | filename, ext = filename.rsplit(u('.'), 1) 7 | return u('.').join([filename, new_extension]) 8 | 9 | 10 | def touch_file(filename): 11 | with open(filename, 'a'): 12 | pass 13 | -------------------------------------------------------------------------------- /python_fu/module.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from keyword import iskeyword 4 | 5 | from .commandline import info, warning 6 | from .compat import PY3, u 7 | from .helpers import replace_extension, touch_file 8 | 9 | module_name_re = re.compile('^[a-zA-Z_][a-zA-Z0-9_]*$') 10 | 11 | 12 | def valid_module_name(module_name): 13 | return (module_name_re.match(module_name) and 14 | not iskeyword(module_name) and 15 | not (module_name.startswith('__') and module_name.endswith('__'))) # exclude __init__ and __main__ 16 | 17 | 18 | class Module(object): 19 | ## 20 | # Constructors 21 | @classmethod 22 | def from_file(cls, file_path): 23 | raise NotImplementedError('not yet implemented') 24 | 25 | def __init__(self, module_path): 26 | components = module_path.split('.') 27 | 28 | # Sanity check 29 | if not all([valid_module_name(mod) for mod in components]): 30 | raise ValueError('Invalid module path: {}'.format(module_path)) 31 | 32 | self.components = components 33 | 34 | @property 35 | def module_path(self): 36 | return u('.').join(self.components) 37 | 38 | @property 39 | def parent_components(self): 40 | if len(self.components) <= 1: 41 | return [] 42 | return self.components[:-1] 43 | 44 | @property 45 | def parent_module(self): 46 | if len(self.components) <= 1: 47 | return None 48 | return Module('.'.join(self.components[:-1])) 49 | 50 | @property 51 | def module_name(self): 52 | return self.components[-1] 53 | 54 | def split(self): 55 | return (self.parent_components, self.module_name) 56 | 57 | @property 58 | def module_file(self): 59 | parents, last = self.split() 60 | return os.path.join(*parents + [last + '.py']) 61 | 62 | @property 63 | def module_dir(self): 64 | parent = self.parent_module 65 | if not parent: 66 | return '.' 67 | return parent.package_dir 68 | 69 | @property 70 | def package_file(self): 71 | return os.path.join(self.package_dir, '__init__.py') 72 | 73 | @property 74 | def package_dir(self): 75 | return os.path.join(*self.components) 76 | 77 | def is_package(self): 78 | return os.path.isfile(self.package_file) 79 | 80 | def is_module(self): 81 | return os.path.isfile(self.module_file) 82 | 83 | def exists(self): 84 | return self.is_package() or self.is_module() 85 | 86 | def create(self, promote=False): 87 | parent_module = self.parent_module 88 | if parent_module: 89 | parent_module.create(promote=True) 90 | 91 | if self.exists(): 92 | # This module exists already, so we're done, unless we need to 93 | # promote it now. 94 | if not self.is_package() and promote: 95 | self.promote() 96 | 97 | return 98 | 99 | # If it does not exist, we need to create it now. We may assume the 100 | # parent directory exists by now. 101 | if not os.path.exists(self.module_file): 102 | touch_file(self.module_file) 103 | 104 | # It's a bit of a short-hand, but it works for now 105 | if promote: 106 | self.promote() 107 | 108 | def promote(self): 109 | if self.is_package(): 110 | warning('{} is a package already, skipping.'.format(self)) 111 | return 112 | 113 | if not self.is_module(): 114 | warning('{} does not exist, skipping.'.format(self)) 115 | return 116 | 117 | module_file = self.module_file 118 | package_file = self.package_file 119 | 120 | info('Promoting {} -> {}'.format(module_file, package_file)) 121 | os.renames(module_file, package_file) 122 | 123 | compiled_extensions = ['pyo', 'pyc'] 124 | for ext in compiled_extensions: 125 | filename = replace_extension(module_file, ext) 126 | if os.path.isfile(filename): 127 | info('Cleaning up compiled self file {}'.format(filename)) 128 | os.remove(filename) 129 | 130 | def demote(self): 131 | if self.is_module(): 132 | warning('{} is a non-package module already, skipping.'.format(self)) 133 | return 134 | 135 | if not self.is_package(): 136 | warning('{} does not exist, skipping.'.format(self)) 137 | return 138 | 139 | module_file = self.module_file 140 | package_file = self.package_file 141 | 142 | # Sanity check: only allow demotes for packages that contain an 143 | # __init__.py file, and nothing else 144 | pkgdir = os.path.dirname(package_file) 145 | package_files = set(os.listdir(pkgdir)) 146 | allowed_junk = set(['__init__.pyc', '__init__.pyo', '.DS_Store']) 147 | 148 | superflous = package_files - allowed_junk - set(['__init__.py']) 149 | if superflous: 150 | warning('Directory {!r} is not empty. Cannot demote, skipping.'.format(pkgdir)) 151 | for file in superflous: 152 | warning('- {}'.format(file)) 153 | return 154 | 155 | for junkfile in allowed_junk: 156 | junkfile = os.path.join(pkgdir, junkfile) 157 | if os.path.isfile(junkfile): 158 | info('Cleaning up junk file {}'.format(junkfile)) 159 | os.remove(junkfile) 160 | 161 | info('Moving {} -> {}'.format(package_file, module_file)) 162 | os.rename(package_file, module_file) 163 | 164 | # Remove the package directory, if it's empty 165 | os.rmdir(pkgdir) 166 | 167 | # Self-printing 168 | if not PY3: # noqa 169 | def __unicode__(self): 170 | return self.module_path 171 | 172 | def __str__(self): 173 | return unicode(self).encode('utf-8') 174 | else: 175 | def __str__(self): # noqa 176 | return self.module_path 177 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | python-fu: Python command line tools, for increased fu. 3 | """ 4 | import sys 5 | 6 | from setuptools import find_packages, setup 7 | 8 | 9 | def get_dependencies(): 10 | dependencies = ['click >= 4.0', 'more_itertools'] 11 | if sys.version_info < (3, 0): 12 | dependencies += ['pathlib'] 13 | return dependencies 14 | 15 | setup( 16 | name='python-fu', 17 | version='0.2.2', 18 | url='https://github.com/nvie/python-fu/', 19 | license='BSD', 20 | author='Vincent Driessen', 21 | author_email='vincent@3rdcloud.com', 22 | description=__doc__, 23 | packages=find_packages(), 24 | entry_points='''\ 25 | [console_scripts] 26 | promote = python_fu.commands.promote:cli 27 | demote = python_fu.commands.demote:cli 28 | mkmodule = python_fu.commands.mkmodule:cli 29 | lsmodules = python_fu.commands.lsmodules:cli 30 | ''', 31 | # include_package_data=True, 32 | zip_safe=False, 33 | platforms='any', 34 | install_requires=get_dependencies(), 35 | classifiers=[ 36 | # As from http://pypi.python.org/pypi?%3Aaction=list_classifiers 37 | #'Development Status :: 1 - Planning', 38 | #'Development Status :: 2 - Pre-Alpha', 39 | #'Development Status :: 3 - Alpha', 40 | #'Development Status :: 4 - Beta', 41 | 'Development Status :: 5 - Production/Stable', 42 | #'Development Status :: 6 - Mature', 43 | #'Development Status :: 7 - Inactive', 44 | 'Programming Language :: Python', 45 | 'Programming Language :: Python :: 2', 46 | #'Programming Language :: Python :: 2.3', 47 | #'Programming Language :: Python :: 2.4', 48 | 'Programming Language :: Python :: 2.5', 49 | 'Programming Language :: Python :: 2.6', 50 | 'Programming Language :: Python :: 2.7', 51 | 'Programming Language :: Python :: 3', 52 | 'Programming Language :: Python :: 3.0', 53 | 'Programming Language :: Python :: 3.1', 54 | 'Programming Language :: Python :: 3.2', 55 | 'Programming Language :: Python :: 3.3', 56 | 'Programming Language :: Python :: Implementation :: PyPy', 57 | 'Intended Audience :: Developers', 58 | 'Intended Audience :: System Administrators', 59 | 'License :: OSI Approved :: BSD License', 60 | 'Operating System :: OS Independent', 61 | 'Topic :: System :: Systems Administration', 62 | ] 63 | ) 64 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nvie/python-fu/2c92fe499e7b641582fde5d93eca29b26499076d/tests/__init__.py -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, with_statement 2 | import os 3 | import hashlib 4 | import unittest 5 | from tempfile import mkdtemp 6 | from shutil import rmtree 7 | 8 | 9 | def walk_files(): 10 | for root, _, files in os.walk('.'): 11 | for file in sorted(files): 12 | yield os.path.normpath(os.path.join(root, file)) 13 | 14 | 15 | def create_dummy_file(filename, contents=''): 16 | with open(filename, 'w') as f: 17 | f.write(contents) 18 | 19 | 20 | def file_sha(filename): 21 | sha1 = hashlib.sha1() 22 | with open(filename, 'rb') as f: 23 | sha1.update(f.read()) 24 | return sha1.hexdigest() 25 | 26 | 27 | class SandboxedTestCase(unittest.TestCase): 28 | def setUp(self): 29 | self.tmpdir = mkdtemp() 30 | self.cwd = os.getcwd() 31 | os.chdir(self.tmpdir) 32 | 33 | def tearDown(self): 34 | os.chdir(self.cwd) 35 | rmtree(self.tmpdir) 36 | 37 | def assertFileTree(self, list_of_files): 38 | actual_file_list = sorted(walk_files()) 39 | assert actual_file_list == sorted(list_of_files) 40 | -------------------------------------------------------------------------------- /tests/test_create.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .helpers import SandboxedTestCase 3 | from python_fu.module import Module 4 | 5 | 6 | class TestCreateModule(SandboxedTestCase): 7 | def test_module_create(self): 8 | """Parent modules can be accessed from any module.""" 9 | Module('foo').create() 10 | self.assertFileTree(['foo.py']) 11 | 12 | Module('bar').create() 13 | self.assertFileTree(['foo.py', 'bar.py']) 14 | 15 | Module('qux').create(promote=True) 16 | self.assertFileTree(['foo.py', 'bar.py', 'qux/__init__.py']) 17 | 18 | Module('foo.bar').create() 19 | self.assertFileTree(['foo/__init__.py', 'foo/bar.py', 'bar.py', 'qux/__init__.py']) 20 | 21 | def test_create_dash_p(self): 22 | """Create with promote option "just promoted" when module already exists.""" 23 | 24 | # Setup, let's create a simple module 25 | Module('foo.bar.qux').create() 26 | self.assertFileTree([ 27 | 'foo/__init__.py', 28 | 'foo/bar/__init__.py', 29 | 'foo/bar/qux.py']) 30 | 31 | # If we now "mkmodule -p foo.bar.qux", qux should simply be promoted 32 | Module('foo.bar.qux').create(promote=True) 33 | self.assertFileTree([ 34 | 'foo/__init__.py', 35 | 'foo/bar/__init__.py', 36 | 'foo/bar/qux/__init__.py']) 37 | -------------------------------------------------------------------------------- /tests/test_demote.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .helpers import SandboxedTestCase 3 | #from .helpers import create_dummy_file, file_sha 4 | from python_fu.module import Module 5 | 6 | 7 | class TestDemotion(SandboxedTestCase): 8 | def test_module_demote(self): 9 | """Packages can be demoted.""" 10 | Module('foo').create(promote=True) 11 | self.assertFileTree(['foo/__init__.py']) 12 | 13 | Module('foo').demote() 14 | self.assertFileTree(['foo.py']) 15 | 16 | def test_nested_module_demote(self): 17 | """Nested packages can be demoted.""" 18 | Module('foo.bar.qux').create(promote=True) 19 | self.assertFileTree([ 20 | 'foo/__init__.py', 21 | 'foo/bar/__init__.py', 22 | 'foo/bar/qux/__init__.py', 23 | ]) 24 | 25 | Module('foo.bar.qux').demote() 26 | self.assertFileTree([ 27 | 'foo/__init__.py', 28 | 'foo/bar/__init__.py', 29 | 'foo/bar/qux.py', 30 | ]) 31 | -------------------------------------------------------------------------------- /tests/test_module.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import unittest 3 | from python_fu.module import Module 4 | 5 | 6 | class TestModule(unittest.TestCase): 7 | def test_invalid_module_names(self): 8 | """Invalid module names raise Exceptions.""" 9 | self.assertRaises(ValueError, Module, '') 10 | self.assertRaises(ValueError, Module, 'names-with-dashes') 11 | self.assertRaises(ValueError, Module, 'names with spaces') 12 | self.assertRaises(ValueError, Module, 'names.with,punctuations!') 13 | self.assertRaises(ValueError, Module, '4names_starting_with_numbers') 14 | self.assertRaises(ValueError, Module, 'names.with.reserved.keywords') 15 | 16 | def test_module_initialization(self): 17 | """Modules can be initialized with strings.""" 18 | m = Module('foo') 19 | assert str(m) == 'foo' 20 | 21 | m = Module('foo.bar') 22 | assert str(m) == 'foo.bar' 23 | 24 | m = Module('foo.bar.qux') 25 | assert str(m) == 'foo.bar.qux' 26 | 27 | def test_module_get_parent(self): 28 | """Parent modules can be accessed from any module.""" 29 | m = Module('foo') 30 | assert m.parent_module is None 31 | 32 | m = Module('foo.bar') 33 | assert str(m.parent_module) == 'foo' 34 | 35 | m = Module('foo.bar.qux') 36 | assert str(m.parent_module) == 'foo.bar' 37 | assert str(m.module_name) == 'qux' 38 | -------------------------------------------------------------------------------- /tests/test_promote.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .helpers import SandboxedTestCase, create_dummy_file, file_sha 3 | from python_fu.module import Module 4 | 5 | 6 | class TestPromotion(SandboxedTestCase): 7 | def test_module_promote(self): 8 | """Modules can be promoted.""" 9 | Module('foo').create() 10 | self.assertFileTree(['foo.py']) 11 | 12 | Module('foo').promote() 13 | self.assertFileTree(['foo/__init__.py']) 14 | 15 | Module('bar').create() 16 | self.assertFileTree(['foo/__init__.py', 'bar.py']) 17 | 18 | def test_module_promote_removes_compiled_files(self): 19 | """Modules can be promoted.""" 20 | Module('foo').create() 21 | create_dummy_file('foo.py', 'print "Hello"') 22 | create_dummy_file('foo.pyc', 'SENTINEL') 23 | self.assertFileTree(['foo.py', 'foo.pyc']) 24 | 25 | Module('foo').promote() 26 | self.assertFileTree(['foo/__init__.py']) # pyc file removed 27 | 28 | def test_module_promotion_preserves_contents(self): 29 | """Promotion preserves file content.""" 30 | create_dummy_file('foo.py', 'print "Hello, world"') 31 | sha1 = file_sha('foo.py') 32 | self.assertFileTree(['foo.py']) 33 | 34 | Module('foo').promote() 35 | self.assertFileTree(['foo/__init__.py']) 36 | sha2 = file_sha('foo/__init__.py') 37 | 38 | assert sha1 == sha2 39 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py34, pypy 3 | 4 | [testenv] 5 | deps=pytest 6 | commands=py.test 7 | --------------------------------------------------------------------------------