├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.md ├── docs ├── Makefile ├── conf.py ├── crashcourse.rst ├── crashcourse1.png ├── crashcourse2.png ├── crashcourse3.png ├── crashcourse3a.png ├── crashcourse4.png ├── crashcourse5.png ├── crashcourse6.png ├── crashcourse7.png ├── flags.rst ├── index.rst └── make.bat ├── moult ├── __init__.py ├── __main__.py ├── args.py ├── ast_scanner.py ├── classes.py ├── color.py ├── compat.py ├── exceptions.py ├── filesystem_scanner.py ├── frameworks │ ├── __init__.py │ └── django.py ├── log.py ├── pip_importer.py ├── printer.py ├── program.py └── utils.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── conftest.py ├── data │ └── scripts │ │ ├── loose │ │ ├── bad_tabs.py │ │ ├── obvious_script.py │ │ ├── unicode.py │ │ └── unicode_import.py │ │ ├── project │ │ ├── .hg │ │ │ └── dont_scan_this.py │ │ ├── .svn │ │ │ └── dont_scan_this.py │ │ ├── .tox │ │ │ └── dont_scan_this.py │ │ ├── CVS │ │ │ └── dont_scan_this.py │ │ ├── __pycache__ │ │ │ └── dont_scan_this.py │ │ ├── django_project │ │ │ ├── README │ │ │ ├── django_project │ │ │ │ ├── __init__.py │ │ │ │ ├── settings.py │ │ │ │ ├── urls.py │ │ │ │ └── wsgi.py │ │ │ ├── manage.py │ │ │ └── testapp │ │ │ │ ├── __init__.py │ │ │ │ ├── admin.py │ │ │ │ ├── custom_db │ │ │ │ ├── __init__.py │ │ │ │ └── base.py │ │ │ │ ├── management │ │ │ │ ├── __init__.py │ │ │ │ └── commands │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── s3.py │ │ │ │ ├── migrations │ │ │ │ └── __init__.py │ │ │ │ ├── models.py │ │ │ │ ├── tests.py │ │ │ │ └── views.py │ │ └── nested │ │ │ └── scripts │ │ │ └── testmodule │ │ │ ├── __init__.py │ │ │ └── utils │ │ │ ├── __init__.py │ │ │ └── spam.py │ │ ├── readonly │ │ ├── readonly1.txt │ │ ├── readonly2.txt │ │ └── readonly3.txt │ │ └── shell │ │ ├── bash_script │ │ └── python_script ├── test_classes.py ├── test_scanning.py └── test_utils.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | install: pip install tox 4 | 5 | script: tox 6 | 7 | env: 8 | - TOXENV=pep8 9 | - TOXENV=py27 10 | - TOXENV=py33 11 | - TOXENV=py34 12 | - TOXENV=pypy 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2015 Tommy Allen 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include README.md 3 | 4 | prune tests 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test clean docs pypi 2 | 3 | test: 4 | python setup.py test 5 | 6 | clean: 7 | rm -rf dist *.egg-info 8 | 9 | docs: 10 | sphinx-build --version 11 | $(MAKE) -C docs html 12 | 13 | pypi: 14 | pandoc -v >/dev/null 15 | python setup.py sdist upload -r pypi 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moult 2 | 3 | [![Build Status](https://travis-ci.org/tweekmonster/moult.svg?branch=develop)](https://travis-ci.org/tweekmonster/moult) 4 | 5 | Moult is a utility that can assist you in finding packages that may not be in use any more. It was created to help me clean up a project's requirements.txt file after a major overhaul. It's far from perfect, but it's a lot faster than figuring out what's actually needed in a `pip freeze` print out. 6 | 7 | ## Requirements 8 | 9 | * `Python 2.7+` 10 | * `pip 1.3+` is required, but is not listed as a dependency so you aren't forced to upgrade your installed version. 11 | 12 | ## Installation 13 | 14 | Since you definitely have pip installed, you can run: `pip install moult` 15 | 16 | ## Handy Features 17 | 18 | * Can be installed globally and ran in Virtual Environments 19 | * Displays package dependencies 20 | * Suggests packages that can be removed 21 | * Search for installed packages using their package name or import path 22 | * Scan your project directories or files to see what packages they are using 23 | * Detects and loads Django settings to see what optional packages are in use 24 | 25 | ## Command Line Interface: 26 | 27 | ``` 28 | usage: moult [-h] [-V] [-s pkg [pkg ...]] [-l] [-a] [-f] [-r] [-v] [-p] [-d] 29 | [--no-color | --no-colour] 30 | [scan [scan ...]] 31 | 32 | A utility for finding Python packages that may not be in use. 33 | 34 | positional arguments: 35 | scan Scans one or more directories or python files to determine 36 | what packages they are using. 37 | 38 | optional arguments: 39 | -h, --help show this help message and exit 40 | -V, --version show program's version number and exit 41 | -s pkg [pkg ...] Packages to search and check. Can be the package name or 42 | import path. Hidden packages can be found, but will not be 43 | a suggested removal without the -a flag. 44 | -l Display local modules only. 45 | -a Display hidden packages. Packages are hidden if they 46 | installed scripts outside of their package directory, or 47 | are hard coded as packages that aren't likely to be 48 | imported by your scripts (virtualenv, pip, supervisor, 49 | etc). When using the -p flag, hidden packages are prefixed 50 | with an underscore so you are less likely to uninstall 51 | them on accident. 52 | -f, --freeze Print requirements like pip does, except for scanned 53 | files. Requires scanned files to work. If no files or 54 | directories are supplied for a scan, the current directory 55 | will be scanned. Packages are sorted so that dependencies 56 | are installed before dependnat packages. Flags below this 57 | are ignored if enabled. 58 | -r Recursively display removable packages. 59 | -v Set verbosity level. -vv will include debug messages. 60 | -p Prints a plain list of removable packages that's suitable 61 | for copy and paste in the command line. Flags below this 62 | are ignored if enabled. 63 | -d Display detailed package dependencies. 64 | --no-color Disable colored output. 65 | --no-colour The classier way to disable colored output. 66 | 67 | moult uses `pip` to find installed packages and determine which ones are not 68 | in use. Unfortunately, not all packages install their dependencies for you. In 69 | those cases, pip, and in turn moult, will have no clue. You must use your own 70 | judgment to determine whether or not the packages listed by moult can actually 71 | be removed without affecting your scripts. 72 | 73 | Again, moult is helpful for listing packages that *may not* be in use. It is a 74 | convenience and the output should not be blindly trusted. 75 | ``` 76 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Moult.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Moult.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Moult" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Moult" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Moult documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Apr 27 21:12:51 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import shlex 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | #sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # The suffix(es) of source filenames. 38 | # You can specify multiple suffix as a list of string: 39 | # source_suffix = ['.rst', '.md'] 40 | source_suffix = '.rst' 41 | 42 | # The encoding of source files. 43 | #source_encoding = 'utf-8-sig' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = u'Moult' 50 | copyright = u'2015, Tommy Allen' 51 | author = u'Tommy Allen' 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | version = '0.1' 59 | # The full version, including alpha/beta/rc tags. 60 | release = '0.1' 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # 65 | # This is also used if you do content translation via gettext catalogs. 66 | # Usually you set "language" from the command line for these cases. 67 | language = None 68 | 69 | # There are two options for replacing |today|: either, you set today to some 70 | # non-false value, then it is used: 71 | #today = '' 72 | # Else, today_fmt is used as the format for a strftime call. 73 | #today_fmt = '%B %d, %Y' 74 | 75 | # List of patterns, relative to source directory, that match files and 76 | # directories to ignore when looking for source files. 77 | exclude_patterns = ['_build'] 78 | 79 | # The reST default role (used for this markup: `text`) to use for all 80 | # documents. 81 | #default_role = None 82 | 83 | # If true, '()' will be appended to :func: etc. cross-reference text. 84 | #add_function_parentheses = True 85 | 86 | # If true, the current module name will be prepended to all description 87 | # unit titles (such as .. function::). 88 | #add_module_names = True 89 | 90 | # If true, sectionauthor and moduleauthor directives will be shown in the 91 | # output. They are ignored by default. 92 | #show_authors = False 93 | 94 | # The name of the Pygments (syntax highlighting) style to use. 95 | pygments_style = 'sphinx' 96 | 97 | # A list of ignored prefixes for module index sorting. 98 | #modindex_common_prefix = [] 99 | 100 | # If true, keep warnings as "system message" paragraphs in the built documents. 101 | #keep_warnings = False 102 | 103 | # If true, `todo` and `todoList` produce output, else they produce nothing. 104 | todo_include_todos = False 105 | 106 | 107 | # -- Options for HTML output ---------------------------------------------- 108 | 109 | # The theme to use for HTML and HTML Help pages. See the documentation for 110 | # a list of builtin themes. 111 | html_theme = 'sphinx_rtd_theme' 112 | 113 | # Theme options are theme-specific and customize the look and feel of a theme 114 | # further. For a list of options available for each theme, see the 115 | # documentation. 116 | #html_theme_options = {} 117 | 118 | # Add any paths that contain custom themes here, relative to this directory. 119 | #html_theme_path = [] 120 | 121 | # The name for this set of Sphinx documents. If None, it defaults to 122 | # " v documentation". 123 | #html_title = None 124 | 125 | # A shorter title for the navigation bar. Default is the same as html_title. 126 | #html_short_title = None 127 | 128 | # The name of an image file (relative to this directory) to place at the top 129 | # of the sidebar. 130 | #html_logo = None 131 | 132 | # The name of an image file (within the static path) to use as favicon of the 133 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 134 | # pixels large. 135 | #html_favicon = None 136 | 137 | # Add any paths that contain custom static files (such as style sheets) here, 138 | # relative to this directory. They are copied after the builtin static files, 139 | # so a file named "default.css" will overwrite the builtin "default.css". 140 | html_static_path = ['_static'] 141 | 142 | # Add any extra paths that contain custom files (such as robots.txt or 143 | # .htaccess) here, relative to this directory. These files are copied 144 | # directly to the root of the documentation. 145 | #html_extra_path = [] 146 | 147 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 148 | # using the given strftime format. 149 | #html_last_updated_fmt = '%b %d, %Y' 150 | 151 | # If true, SmartyPants will be used to convert quotes and dashes to 152 | # typographically correct entities. 153 | #html_use_smartypants = True 154 | 155 | # Custom sidebar templates, maps document names to template names. 156 | #html_sidebars = {} 157 | 158 | # Additional templates that should be rendered to pages, maps page names to 159 | # template names. 160 | #html_additional_pages = {} 161 | 162 | # If false, no module index is generated. 163 | #html_domain_indices = True 164 | 165 | # If false, no index is generated. 166 | #html_use_index = True 167 | 168 | # If true, the index is split into individual pages for each letter. 169 | #html_split_index = False 170 | 171 | # If true, links to the reST sources are added to the pages. 172 | #html_show_sourcelink = True 173 | 174 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 175 | #html_show_sphinx = True 176 | 177 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 178 | #html_show_copyright = True 179 | 180 | # If true, an OpenSearch description file will be output, and all pages will 181 | # contain a tag referring to it. The value of this option must be the 182 | # base URL from which the finished HTML is served. 183 | #html_use_opensearch = '' 184 | 185 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 186 | #html_file_suffix = None 187 | 188 | # Language to be used for generating the HTML full-text search index. 189 | # Sphinx supports the following languages: 190 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 191 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 192 | #html_search_language = 'en' 193 | 194 | # A dictionary with options for the search language support, empty by default. 195 | # Now only 'ja' uses this config value 196 | #html_search_options = {'type': 'default'} 197 | 198 | # The name of a javascript file (relative to the configuration directory) that 199 | # implements a search results scorer. If empty, the default will be used. 200 | #html_search_scorer = 'scorer.js' 201 | 202 | # Output file base name for HTML help builder. 203 | htmlhelp_basename = 'Moultdoc' 204 | 205 | # -- Options for LaTeX output --------------------------------------------- 206 | 207 | latex_elements = { 208 | # The paper size ('letterpaper' or 'a4paper'). 209 | #'papersize': 'letterpaper', 210 | 211 | # The font size ('10pt', '11pt' or '12pt'). 212 | #'pointsize': '10pt', 213 | 214 | # Additional stuff for the LaTeX preamble. 215 | #'preamble': '', 216 | 217 | # Latex figure (float) alignment 218 | #'figure_align': 'htbp', 219 | } 220 | 221 | # Grouping the document tree into LaTeX files. List of tuples 222 | # (source start file, target name, title, 223 | # author, documentclass [howto, manual, or own class]). 224 | latex_documents = [ 225 | (master_doc, 'Moult.tex', u'Moult Documentation', 226 | u'Tommy Allen', 'manual'), 227 | ] 228 | 229 | # The name of an image file (relative to this directory) to place at the top of 230 | # the title page. 231 | #latex_logo = None 232 | 233 | # For "manual" documents, if this is true, then toplevel headings are parts, 234 | # not chapters. 235 | #latex_use_parts = False 236 | 237 | # If true, show page references after internal links. 238 | #latex_show_pagerefs = False 239 | 240 | # If true, show URL addresses after external links. 241 | #latex_show_urls = False 242 | 243 | # Documents to append as an appendix to all manuals. 244 | #latex_appendices = [] 245 | 246 | # If false, no module index is generated. 247 | #latex_domain_indices = True 248 | 249 | 250 | # -- Options for manual page output --------------------------------------- 251 | 252 | # One entry per manual page. List of tuples 253 | # (source start file, name, description, authors, manual section). 254 | man_pages = [ 255 | (master_doc, 'moult', u'Moult Documentation', 256 | [author], 1) 257 | ] 258 | 259 | # If true, show URL addresses after external links. 260 | #man_show_urls = False 261 | 262 | 263 | # -- Options for Texinfo output ------------------------------------------- 264 | 265 | # Grouping the document tree into Texinfo files. List of tuples 266 | # (source start file, target name, title, author, 267 | # dir menu entry, description, category) 268 | texinfo_documents = [ 269 | (master_doc, 'Moult', u'Moult Documentation', 270 | author, 'Moult', 'One line description of project.', 271 | 'Miscellaneous'), 272 | ] 273 | 274 | # Documents to append as an appendix to all manuals. 275 | #texinfo_appendices = [] 276 | 277 | # If false, no module index is generated. 278 | #texinfo_domain_indices = True 279 | 280 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 281 | #texinfo_show_urls = 'footnote' 282 | 283 | # If true, do not generate a @detailmenu in the "Top" node's menu. 284 | #texinfo_no_detailmenu = False 285 | -------------------------------------------------------------------------------- /docs/crashcourse.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | Crash Course 3 | ************ 4 | 5 | Installation 6 | ------------ 7 | 8 | Start by installing moult via ``pip install moult`` 9 | 10 | **Note**: You *must* already have :command:`pip` installed. It is not 11 | installed as a requirement for :command:`moult` since :command:`pip` may 12 | attempt to upgrade itself in the process. 13 | 14 | 15 | Displaying packages that can be removed 16 | --------------------------------------- 17 | 18 | To display packages that can be removed, run ``moult`` with no options. 19 | 20 | .. image:: crashcourse1.png 21 | 22 | Moult reports that the packages :file:`MySQL-python` and 23 | :file:`django-allauth`. 24 | 25 | You can display more detailed information about the packages by using the ``-d`` flag. 26 | 27 | .. image:: crashcourse2.png 28 | 29 | As you can see, it appears that removing :file:`MySQL-python` will leave 30 | nothing behind. But, :file:`django-allauth` would leave a few packages 31 | behind if you removed it. 32 | 33 | To display just how far you can go with removals, you can use the ``-r`` flag. 34 | 35 | .. image:: crashcourse3.png 36 | 37 | What you see here is all of the packages that can be removed. You may have 38 | noticed that :file:`Django` was not displayed as a package that could be 39 | removed. This is because it's a :ref:`hidden package`. To 40 | display hidden packages, add the ``-a`` flag. 41 | 42 | .. image:: crashcourse4.png 43 | 44 | Suddenly there's a lot more packages being suggested. Packages like :file:`celery` 45 | and :file:`supervisor` are hidden because they appear to be standalone packages 46 | (they have shell scripts). Suppose you want to keep :file:`supervisor`, but want 47 | to remove :file:`celery`, you could run: ``moult -s celery -ar`` 48 | 49 | .. image:: crashcourse5.png 50 | 51 | :command:`moult` displays all the packages that can be removed if you were to 52 | remove :file:`celery`. If you're anything like me, you would remove :file:`celery` 53 | and later run ``pip freeze``, see the left over packages, and start Googling 54 | them to see what installed them. No more of that! 55 | 56 | Here's one more example where removing a package would normally leave you 57 | scratching your head late at night: 58 | 59 | .. image:: crashcourse6.png 60 | 61 | There might've been times when you saw something like :file:`oauthlib` 62 | and decided to leave it around because it *sounded important* to 63 | *some package*. Now you know for sure that it can go. 64 | 65 | Scanning your projects 66 | ---------------------- 67 | 68 | Seeing what packages can be removed is great and all, but what about **your** 69 | project's package requirements? 70 | 71 | :command:`moult` can be supplied with a directory or file to scan for imports. 72 | Below is a scan of a simple Django project. 73 | 74 | .. image:: crashcourse7.png 75 | 76 | Contrary to what the first command in this crash course showed, 77 | :file:`MySQL-python` and :file:`django-allauth` are actually needed by 78 | this Django project. :command:`moult` also noticed that this 79 | directory contained a Django project and loaded its settings to 80 | determine what packages the project was configured to use. 81 | -------------------------------------------------------------------------------- /docs/crashcourse1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweekmonster/moult/38d3a3b9002336219897ebe263ca1d8dcadbecf5/docs/crashcourse1.png -------------------------------------------------------------------------------- /docs/crashcourse2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweekmonster/moult/38d3a3b9002336219897ebe263ca1d8dcadbecf5/docs/crashcourse2.png -------------------------------------------------------------------------------- /docs/crashcourse3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweekmonster/moult/38d3a3b9002336219897ebe263ca1d8dcadbecf5/docs/crashcourse3.png -------------------------------------------------------------------------------- /docs/crashcourse3a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweekmonster/moult/38d3a3b9002336219897ebe263ca1d8dcadbecf5/docs/crashcourse3a.png -------------------------------------------------------------------------------- /docs/crashcourse4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweekmonster/moult/38d3a3b9002336219897ebe263ca1d8dcadbecf5/docs/crashcourse4.png -------------------------------------------------------------------------------- /docs/crashcourse5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweekmonster/moult/38d3a3b9002336219897ebe263ca1d8dcadbecf5/docs/crashcourse5.png -------------------------------------------------------------------------------- /docs/crashcourse6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweekmonster/moult/38d3a3b9002336219897ebe263ca1d8dcadbecf5/docs/crashcourse6.png -------------------------------------------------------------------------------- /docs/crashcourse7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweekmonster/moult/38d3a3b9002336219897ebe263ca1d8dcadbecf5/docs/crashcourse7.png -------------------------------------------------------------------------------- /docs/flags.rst: -------------------------------------------------------------------------------- 1 | ******************** 2 | Command Line Options 3 | ******************** 4 | 5 | .. code-block:: none 6 | 7 | positional arguments: 8 | scan Scans one or more directories or python files to determine 9 | what packages they are using. 10 | 11 | optional arguments: 12 | -h, --help show this help message and exit 13 | -V, --version show program's version number and exit 14 | -s pkg [pkg ...] Packages to search and check. Can be the package name or 15 | import path. Hidden packages can be found, but will not be 16 | a suggested removal without the -a flag. 17 | -l Display local modules only. 18 | -a Display hidden packages. Packages are hidden if they 19 | installed scripts outside of their package directory, or 20 | are hard coded as packages that aren't likely to be 21 | imported by your scripts (virtualenv, pip, supervisor, 22 | etc). When using the -p flag, hidden packages are prefixed 23 | with an underscore so you are less likely to uninstall 24 | them on accident. 25 | -f, --freeze Print requirements like pip does, except for scanned 26 | files. Requires scanned files to work. If no files or 27 | directories are supplied for a scan, the current directory 28 | will be scanned. Packages are sorted so that dependencies 29 | are installed before dependnat packages. Flags below this 30 | are ignored if enabled. 31 | -r Recursively display removable packages. 32 | -v Set verbosity level. -vv will include debug messages. 33 | -p Prints a plain list of removable packages that's suitable 34 | for copy and paste in the command line. Flags below this 35 | are ignored if enabled. 36 | -d Display detailed package dependencies. 37 | --no-color Disable colored output. 38 | --no-colour The classier way to disable colored output. 39 | 40 | 41 | Detailed Explanations 42 | ===================== 43 | 44 | Some options that need a little more explanation than what the help output 45 | provides. 46 | 47 | **scan** 48 | Optional positional arguments consisting of directories or files you want 49 | to scan. The printed results will list used packages under the top level 50 | directory or script names in your scan. 51 | 52 | Files are scanned for import statements and matched against installed 53 | packages. If no installed package is found, the import is silently ignored. 54 | 55 | If a :file:`settings.py` file is encountered and contains an 56 | ``INSTALLED_APPS`` variable, :command:`moult` will attempt to load it as a 57 | Django project to determine the project's configured package dependencies. 58 | :command:`moult` will first attempt to use Django's 59 | `apps `_ 60 | registry to get the installed apps. If it fails, it will fall back to 61 | directly reading the ``INSTALLED_APPS`` variable. Any complication in 62 | loading the settings will be printed to the console. 63 | 64 | **-s** 65 | Search for a package. You can supply either the package name or import path 66 | of a module. When searching for an import path, moult will find the package 67 | that defines a top level module matching the import path. 68 | 69 | **-l** 70 | If you created a virutalenv with the :option:`--system-site-packages` 71 | flag, this means that the system's site-packages are visible to 72 | :command:`pip` and :command:`moult`. Enabling this flag tells 73 | :command:`moult` to ignore packages that exist outside of your virtualenv. 74 | 75 | .. _show-all: 76 | 77 | **-a** 78 | Enabling this flag will display hidden packages. Packages are hidden 79 | either by :command:`pip`'s hard coded ignored packages or if they have 80 | installed scripts that exist out side of the package's import path. Extra 81 | package files are considered scripts if they contain a shebang (#!) in the 82 | first 2 bytes of the file. It is assumed that if a package installed 83 | scripts, the package's purpose goes beyond being imported in your scripts. 84 | When combined with the :option:`-p` flag, package names will be prefixed 85 | with an underscore to avoid accidental removals if you eagerly copy and 86 | pasted the output when running :command:`pip uninstall`. 87 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ***** 2 | Moult 3 | ***** 4 | 5 | A utility for finding Python packages that may not be in use. 6 | 7 | When you uninstall a package via :program:`pip`, you will be left with its dependency 8 | packages (for good reason). It's not the end of the world, but in projects 9 | where you want to maintain a :file:`requirements.txt` file by using the output of 10 | :program:`pip freeze`, it can be pretty frustrating to figure out which one of the 11 | packages is actually used by your project or the packages your project depends 12 | on. 13 | 14 | Moult can help by scanning your project files and printing the packages that 15 | appears to have no relation to your project. 16 | 17 | .. toctree:: 18 | :maxdepth: 2 19 | 20 | flags.rst 21 | crashcourse.rst 22 | 23 | 24 | Links 25 | ===== 26 | 27 | * PyPI: ``_ 28 | * Source Code: ``_ 29 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Moult.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Moult.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /moult/__init__.py: -------------------------------------------------------------------------------- 1 | '''A utility for finding Python packages that may not be in use. 2 | ''' 3 | from __future__ import print_function 4 | 5 | import os 6 | import sys 7 | import codecs 8 | 9 | 10 | __all__ = ('__version__', 'main') 11 | __version__ = '0.1.2' 12 | 13 | 14 | if sys.stdout.encoding is None: 15 | sys.stdout = codecs.getwriter('utf8')(sys.stdout) 16 | if sys.stderr.encoding is None: 17 | sys.stderr = codecs.getwriter('utf8')(sys.stderr) 18 | 19 | 20 | def is_venv(): 21 | '''Redefinition of pip's running_under_virtualenv(). 22 | ''' 23 | return hasattr(sys, 'real_prefix') \ 24 | or sys.prefix != getattr(sys, 'base_prefix', sys.prefix) 25 | 26 | 27 | def main(): 28 | if 'VIRTUAL_ENV' in os.environ and not is_venv(): 29 | # Activate the virtualenv before importing moult's program to avoid 30 | # loading modules. 31 | print('Activating', os.environ['VIRTUAL_ENV']) 32 | activate = os.path.join(os.environ['VIRTUAL_ENV'], 'bin', 'activate_this.py') 33 | if os.path.exists(activate): 34 | with open(activate) as fp: 35 | exec(compile(fp.read(), activate, 'exec'), {'__file__': activate}) 36 | 37 | from moult.program import run 38 | return run() 39 | -------------------------------------------------------------------------------- /moult/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from .program import run 4 | 5 | sys.exit(run()) 6 | -------------------------------------------------------------------------------- /moult/args.py: -------------------------------------------------------------------------------- 1 | from . import __version__ 2 | 3 | 4 | def create_argparser(): 5 | import argparse 6 | from argparse import RawDescriptionHelpFormatter 7 | 8 | description = ''' 9 | A utility for finding Python packages that may not be in use. 10 | '''.strip() 11 | 12 | epilog = ''' 13 | moult uses `pip` to find installed packages and determine which ones are not 14 | in use. Unfortunately, not all packages install their dependencies for you. In 15 | those cases, pip, and in turn moult, will have no clue. You must use your own 16 | judgment to determine whether or not the packages listed by moult can actually 17 | be removed without affecting your scripts. 18 | 19 | Again, moult is helpful for listing packages that *may not* be in use. It is a 20 | convenience and the output should not be blindly trusted. 21 | '''.strip() 22 | 23 | parser = argparse.ArgumentParser(prog='moult', description=description, 24 | epilog=epilog, 25 | formatter_class=RawDescriptionHelpFormatter) 26 | 27 | parser.add_argument('-V', '--version', action='version', 28 | version='%(prog)s {}'.format(__version__)) 29 | 30 | parser.add_argument('scan', metavar='scan', nargs='*', 31 | help='Scans one or more directories or python files to' 32 | ' determine what packages they are using.') 33 | 34 | parser.add_argument('-s', metavar='pkg', nargs='+', 35 | dest='packages', required=False, help='Packages to' 36 | ' search and check. Can be the package name or import' 37 | ' path. Hidden packages can be found, but will not be' 38 | ' a suggested removal without the -a flag.') 39 | 40 | parser.add_argument('-l', action='store_true', required=False, 41 | dest='local', help='Display local modules only.') 42 | 43 | description = '''Display hidden packages. Packages are hidden if they 44 | installed scripts outside of their package directory, or are hard coded as 45 | packages that aren't likely to be imported by your scripts (virtualenv, pip, 46 | supervisor, etc). When using the -p flag, hidden packages are prefixed with an 47 | underscore so you are less likely to uninstall them on accident.''' 48 | 49 | parser.add_argument('-a', action='store_true', required=False, 50 | dest='show_all', help=description) 51 | 52 | parser.add_argument('-f', '--freeze', action='store_true', required=False, 53 | dest='freeze', help='Print requirements like pip does,' 54 | ' except for scanned files. Requires scanned files to' 55 | ' work. If no files or directories are supplied for a' 56 | ' scan, the current directory will be scanned.' 57 | ' Packages are sorted so that dependencies are' 58 | ' installed before dependnat packages. Flags below' 59 | ' this are ignored if enabled.') 60 | 61 | parser.add_argument('-r', action='store_true', required=False, 62 | dest='recursive', help='Recursively display removable' 63 | ' packages.') 64 | 65 | parser.add_argument('-v', action='count', required=False, 66 | dest='verbose', help='Set verbosity level. -vv will' 67 | ' include debug messages.') 68 | 69 | parser.add_argument('-p', action='store_true', required=False, 70 | dest='plain', help='Prints a plain list of removable' 71 | ' packages that\'s suitable for copy and paste in the' 72 | ' command line. Flags below this are ignored if' 73 | ' enabled.') 74 | 75 | parser.add_argument('-d', action='store_true', required=False, 76 | dest='detail', help='Display detailed package' 77 | ' dependencies.') 78 | 79 | color = parser.add_mutually_exclusive_group() 80 | 81 | color.add_argument('--no-color', action='store_true', 82 | required=False, dest='no_color', 83 | help='Disable colored output.') 84 | 85 | color.add_argument('--no-colour', action='store_true', 86 | required=False, dest='no_colour', 87 | help='The classier way to disable colored output.') 88 | 89 | return parser 90 | -------------------------------------------------------------------------------- /moult/ast_scanner.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import io 4 | import re 5 | import ast 6 | 7 | from .exceptions import MoultScannerError 8 | from .compat import str_ 9 | from . import utils, log 10 | 11 | 12 | _fallback_re = re.compile(r''' 13 | ^[\ \t]*( 14 | from[\ \t]+[\w\.]+[\ \t]+import\s+\([\s\w,]+\)| 15 | from[\ \t]+[\w\.]+[\ \t]+import[\ \t\w,]+| 16 | import[\ \t]+\([\s\w,]+\)| 17 | import[\ \t]+[\ \t\w,]+ 18 | ) 19 | ''', re.VERBOSE | re.MULTILINE | re.UNICODE) 20 | 21 | 22 | def ast_value(val, scope, return_name=False): 23 | '''Recursively parse out an AST value. This makes no attempt to load 24 | modules or reconstruct functions on purpose. We do not want to 25 | inadvertently call destructive code. 26 | ''' 27 | # :TODO: refactor the hell out of this 28 | try: 29 | if isinstance(val, (ast.Assign, ast.Delete)): 30 | if hasattr(val, 'value'): 31 | value = ast_value(val.value, scope) 32 | else: 33 | value = None 34 | for t in val.targets: 35 | name = ast_value(t, scope, return_name=True) 36 | if isinstance(t.ctx, ast.Del): 37 | if name in scope: 38 | scope.pop(name) 39 | elif isinstance(t.ctx, ast.Store): 40 | scope[name] = value 41 | return 42 | elif isinstance(val, ast.Expr) and isinstance(val.value, ast.Name): 43 | return ast_value(val.value) 44 | 45 | if isinstance(val, ast.Name): 46 | if isinstance(val.ctx, ast.Load): 47 | if val.id == 'None': 48 | return None 49 | elif val.id == 'True': 50 | return True 51 | elif val.id == 'False': 52 | return False 53 | 54 | if val.id in scope: 55 | return scope[val.id] 56 | 57 | if return_name: 58 | return val.id 59 | elif isinstance(val.ctx, ast.Store): 60 | if return_name: 61 | return val.id 62 | return None 63 | 64 | if isinstance(val, ast.Subscript): 65 | toslice = ast_value(val.value, scope) 66 | theslice = ast_value(val.slice, scope) 67 | return toslice[theslice] 68 | elif isinstance(val, ast.Index): 69 | return ast_value(val.value, scope) 70 | elif isinstance(val, ast.Slice): 71 | lower = ast_value(val.lower) 72 | upper = ast_value(val.upper) 73 | step = ast_value(val.step) 74 | return slice(lower, upper, step) 75 | 76 | if isinstance(val, list): 77 | return [ast_value(x, scope) for x in val] 78 | elif isinstance(val, tuple): 79 | return tuple(ast_value(x, scope) for x in val) 80 | 81 | if isinstance(val, ast.Attribute): 82 | name = ast_value(val.value, scope, return_name=True) 83 | if isinstance(val.ctx, ast.Load): 84 | return '.'.join((name, val.attr)) 85 | if return_name: 86 | return name 87 | elif isinstance(val, ast.keyword): 88 | return {val.arg: ast_value(val.value, scope)} 89 | elif isinstance(val, ast.List): 90 | return [ast_value(x, scope) for x in val.elts] 91 | elif isinstance(val, ast.Tuple): 92 | return tuple(ast_value(x, scope) for x in val.elts) 93 | elif isinstance(val, ast.Dict): 94 | return dict(zip([ast_value(x, scope) for x in val.keys], 95 | [ast_value(x, scope) for x in val.values])) 96 | elif isinstance(val, ast.Num): 97 | return val.n 98 | elif isinstance(val, ast.Str): 99 | return val.s 100 | elif hasattr(ast, 'Bytes') and isinstance(val, ast.Bytes): 101 | return bytes(val.s) 102 | except Exception: 103 | # Don't care, just return None 104 | pass 105 | 106 | return None 107 | 108 | 109 | def flatten_call_args(args, kwlist, starargs, kwargs): 110 | if starargs: 111 | args.extend(starargs) 112 | 113 | keywords = {} 114 | for kw in kwlist: 115 | keywords.update(kw) 116 | 117 | if kwargs: 118 | keywords.update(keywords) 119 | 120 | return args, keywords 121 | 122 | 123 | def get_args(args, kwargs, arg_names): 124 | '''Get arguments as a dict. 125 | ''' 126 | n_args = len(arg_names) 127 | if len(args) + len(kwargs) > n_args: 128 | raise MoultScannerError('Too many arguments supplied. Expected: {}'.format(n_args)) 129 | 130 | out_args = {} 131 | for i, a in enumerate(args): 132 | out_args[arg_names[i]] = a 133 | 134 | for a in arg_names: 135 | if a not in out_args: 136 | out_args[a] = None 137 | 138 | out_args.update(kwargs) 139 | return out_args 140 | 141 | 142 | def parse_programmatic_import(node, scope): 143 | name = ast_value(node.func, scope, return_name=True) 144 | if not name: 145 | return [] 146 | 147 | args, kwargs = flatten_call_args(ast_value(node.args, scope), 148 | ast_value(node.keywords, scope), 149 | ast_value(node.starargs, scope), 150 | ast_value(node.kwargs, scope)) 151 | 152 | imports = [] 153 | 154 | if name.endswith('__import__'): 155 | func_args = get_args(args, kwargs, ['name', 'globals', 'locals', 156 | 'fromlist', 'level']) 157 | log.debug('Found `__import__` with args: {}'.format(func_args)) 158 | if not func_args['name']: 159 | raise MoultScannerError('No name supplied for __import__') 160 | if func_args['fromlist']: 161 | if not hasattr(func_args['fromlist'], '__iter__'): 162 | raise MoultScannerError('__import__ fromlist is not iterable type') 163 | for fromname in func_args['fromlist']: 164 | imports.append((func_args['name'], fromname)) 165 | else: 166 | imports.append((None, func_args['name'])) 167 | elif name.endswith('import_module'): 168 | func_args = get_args(args, kwargs, ['name', 'package']) 169 | log.debug('Found `import_module` with args: {}'.format(func_args)) 170 | if not func_args['name']: 171 | raise MoultScannerError('No name supplied for import_module') 172 | if func_args['package'] and not isinstance(func_args['package'], (bytes, str_)): 173 | raise MoultScannerError('import_module package not string type') 174 | imports.append((func_args['package'], func_args['name'])) 175 | 176 | return imports 177 | 178 | 179 | class ResolvedImport(object): 180 | def __init__(self, import_path, import_root): 181 | module = import_path.split('.', 1)[0] 182 | self.module = module 183 | self.import_path = import_path 184 | self.is_stdlib = utils.is_stdlib(module) 185 | self.filename = None 186 | 187 | if not self.is_stdlib: 188 | self.filename = utils.file_containing_import(import_path, import_root) 189 | 190 | def __repr__(self): 191 | return ''.format(self.import_path, self.filename) 192 | 193 | 194 | class ImportNodeVisitor(ast.NodeVisitor): 195 | '''A simplistic AST visitor that looks for easily identified imports. 196 | 197 | It can resolve simple assignment variables defined within the module. 198 | ''' 199 | def reset(self, filename): 200 | self.filename = filename 201 | self.import_path, self.import_root = utils.import_path_from_file(filename) 202 | 203 | def add_import(self, *names): 204 | for module, name in names: 205 | if module and module.startswith('.'): 206 | module = utils.resolve_import(module, self.import_path) 207 | elif not module: 208 | module = '' 209 | module = '.'.join((module, name.strip('.'))).strip('.') 210 | if module not in self._imports: 211 | self._imports.add(module) 212 | self.imports.append(ResolvedImport(module, self.import_root)) 213 | 214 | def visit_Module(self, node): 215 | log.debug('Resetting AST visitor with module path: %s', self.import_path) 216 | self._imports = set() 217 | self.imports = [] 218 | self.scope = {} 219 | if node: 220 | self.generic_visit(node) 221 | 222 | def visit_Import(self, node): 223 | for n in node.names: 224 | self.add_import((n.name, '')) 225 | self.generic_visit(node) 226 | 227 | def visit_ImportFrom(self, node): 228 | module = '{}{}'.format('.' * node.level, str_(node.module or '')) 229 | for n in node.names: 230 | self.add_import((module, n.name)) 231 | self.generic_visit(node) 232 | 233 | def visit_Expr(self, node): 234 | if isinstance(node.value, ast.Call): 235 | try: 236 | self.add_import(*parse_programmatic_import(node.value, self.scope)) 237 | except MoultScannerError as e: 238 | log.debug('%s, File: %s', e, self.filename) 239 | elif isinstance(node.value, ast.Name): 240 | ast_value(node.value, self.scope) 241 | self.generic_visit(node) 242 | 243 | def visit_Assign(self, node): 244 | ast_value(node, self.scope) 245 | 246 | def visit_Delete(self, node): 247 | ast_value(node, self.scope) 248 | 249 | def visit(self, node): 250 | super(ImportNodeVisitor, self).visit(node) 251 | 252 | 253 | ast_visitor = ImportNodeVisitor() 254 | 255 | 256 | def _ast_scan_file_re(filename): 257 | try: 258 | with io.open(filename, 'rt', encoding='utf8') as fp: 259 | script = fp.read() 260 | normalized = '' 261 | for imp in _fallback_re.finditer(script): 262 | imp_line = imp.group(1) 263 | try: 264 | imp_line = imp_line.decode('utf8') 265 | except AttributeError: 266 | pass 267 | except UnicodeEncodeError: 268 | log.warn('Unicode import failed: %s', imp_line) 269 | continue 270 | imp_line = re.sub(r'[\(\)]', '', imp_line) 271 | normalized += ' '.join(imp_line.split()).strip(',') + '\n' 272 | log.debug('Normalized imports:\n%s', normalized) 273 | 274 | try: 275 | root = ast.parse(normalized, filename=filename) 276 | except SyntaxError: 277 | log.error('Could not parse file using regex scan: %s', filename) 278 | log.info('Exception:', exc_info=True) 279 | return None, None 280 | 281 | log.debug('Starting AST Scan (regex): %s', filename) 282 | ast_visitor.reset(filename) 283 | ast_visitor.visit(root) 284 | return ast_visitor.scope, ast_visitor.imports 285 | except IOError: 286 | log.warn('Could not open file: %s', filename) 287 | 288 | return None, None 289 | 290 | 291 | def ast_scan_file(filename, re_fallback=True): 292 | '''Scans a file for imports using AST. 293 | 294 | In addition to normal imports, try to get imports via `__import__` 295 | or `import_module` calls. The AST parser should be able to resolve 296 | simple variable assignments in cases where these functions are called 297 | with variables instead of strings. 298 | ''' 299 | try: 300 | with io.open(filename, 'rb') as fp: 301 | try: 302 | root = ast.parse(fp.read(), filename=filename) 303 | except (SyntaxError, IndentationError): 304 | if re_fallback: 305 | log.debug('Falling back to regex scanner') 306 | return _ast_scan_file_re(filename) 307 | else: 308 | log.error('Could not parse file: %s', filename) 309 | log.info('Exception:', exc_info=True) 310 | return None, None 311 | log.debug('Starting AST Scan: %s', filename) 312 | ast_visitor.reset(filename) 313 | ast_visitor.visit(root) 314 | log.debug('Project path: %s', ast_visitor.import_root) 315 | return ast_visitor.scope, ast_visitor.imports 316 | except IOError: 317 | log.warn('Could not open file: %s', filename) 318 | 319 | return None, None 320 | -------------------------------------------------------------------------------- /moult/classes.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .compat import PY3 4 | 5 | 6 | class PyModule(object): 7 | def __init__(self, name, version, location='', missing=False): 8 | self.name = name 9 | self.import_names = [name] 10 | self.version = version 11 | self.is_scan = version in ('SCRIPT', 'MODULE', 'DIRECTORY') 12 | self.frameworks = [] 13 | self.location = location 14 | self._dependencies = [] # The string list of dependencies 15 | self.dependencies = [] 16 | self.dependants = [] 17 | self._dependants = [] # This is a shadow dependant list for the turtles 18 | self.installed_scripts = [] 19 | self.installed_files = [] 20 | self.user = False 21 | self.local = False 22 | self.hidden = False 23 | self.missing = missing 24 | 25 | def set_import_names(self, names): 26 | self.import_names = [x.replace('/', '.') for x in names] 27 | 28 | def add_framework(self, framework): 29 | if framework not in self.frameworks: 30 | self.frameworks.append(framework) 31 | 32 | def add_dependency(self, dep): 33 | if dep not in self.dependencies: 34 | if dep.is_scan: 35 | self.dependencies.insert(0, dep) 36 | else: 37 | self.dependencies.append(dep) 38 | 39 | def remove_dependency(self, dep): 40 | if dep in self.dependencies: 41 | self.dependencies.remove(dep) 42 | 43 | def add_dependant(self, dep): 44 | if dep not in self.dependants: 45 | if dep.is_scan: 46 | self.dependants.insert(0, dep) 47 | self._dependants.insert(0, dep) 48 | else: 49 | self.dependants.append(dep) 50 | self._dependants.append(dep) 51 | 52 | def remove_dependant(self, dep): 53 | if dep in self._dependants: 54 | self._dependants.remove(dep) 55 | 56 | def restore_dependants(self): 57 | self._dependants = self.dependants[:] 58 | 59 | def __hash__(self): 60 | return hash((self.name, self.version)) 61 | 62 | def __unicode__(self): 63 | if self.is_scan: 64 | fmt = '{name} [{version}]' 65 | else: 66 | fmt = '{name} ({version})' 67 | return fmt.format(name=self.name, version=self.version) 68 | 69 | def __str__(self): 70 | if PY3: 71 | return self.__unicode__() 72 | return self.__unicode__().encode('utf8') 73 | 74 | def __bytes__(self): 75 | return self.__unicode__().encode('utf8') 76 | 77 | def __repr__(self): 78 | r = ''.format(self.__unicode__()) 79 | if PY3: 80 | return r 81 | return r.encode('utf8') 82 | -------------------------------------------------------------------------------- /moult/color.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import sys 4 | 5 | from .compat import PY3, str_ 6 | 7 | 8 | FG_BLACK = 30 9 | FG_RED = 31 10 | FG_GREEN = 32 11 | FG_YELLOW = 33 12 | FG_BLUE = 34 13 | FG_MAGENTA = 35 14 | FG_CYAN = 36 15 | FG_WHITE = 37 16 | FG_RESET = 39 17 | 18 | BG_BLACK = 40 19 | BG_RED = 41 20 | BG_GREEN = 42 21 | BG_YELLOW = 43 22 | BG_BLUE = 44 23 | BG_MAGENTA = 45 24 | BG_CYAN = 46 25 | BG_WHITE = 47 26 | BG_RESET = 49 27 | 28 | 29 | enabled = True 30 | _enabled = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() 31 | 32 | 33 | class ColorCombo(object): 34 | def __init__(self, foreground=0, background=0, bright=None): 35 | self.foreground = foreground or FG_RESET 36 | self.background = background or BG_RESET 37 | self.set_bright(bright) 38 | 39 | def set_bright(self, bright): 40 | if bright is None: 41 | self.flag = 22 42 | elif bright: 43 | self.flag = 1 44 | else: 45 | self.flag = 2 46 | 47 | def copy(self): 48 | c = ColorCombo(self.foreground, self.background) 49 | c.flag = self.flag 50 | return c 51 | 52 | def __repr__(self): 53 | r = ''.format(self.foreground, self.background) 54 | if PY3: 55 | return r 56 | return r.encode('utf8') 57 | 58 | 59 | HEY = ColorCombo(FG_RED) 60 | YAY = ColorCombo(FG_GREEN) 61 | MEH = ColorCombo(FG_YELLOW) 62 | GOOD = ColorCombo(FG_BLUE) 63 | NEAT = ColorCombo(FG_CYAN) 64 | SHHH = ColorCombo(FG_MAGENTA) 65 | NOOO = ColorCombo(FG_WHITE, BG_RED, bright=True) 66 | MAN = ColorCombo(FG_BLACK, BG_YELLOW, bright=True) 67 | 68 | 69 | class ColorTextRun(object): 70 | '''String imposter that supports multiple color strings, mostly so len() 71 | reports the actual text's length 72 | ''' 73 | def __init__(self, *items): 74 | self.items = list(items) 75 | 76 | def __len__(self): 77 | return sum(map(len, self.items)) 78 | 79 | def __unicode__(self): 80 | return str_(''.join(map(str_, self.items))) 81 | 82 | def __str__(self): 83 | if PY3: 84 | return self.__unicode__() 85 | return self.__unicode__().encode('utf8') 86 | 87 | def __repr__(self): 88 | r = ''.format([repr(x) for x in self.items]) 89 | if PY3: 90 | return r 91 | return r.encode('utf8') 92 | 93 | def __add__(self, other): 94 | self.items.append(other) 95 | return self 96 | 97 | def __radd__(self, other): 98 | self.items.insert(0, other) 99 | return self 100 | 101 | def encode(self, *args, **kwargs): 102 | return str_(self).encode(*args, **kwargs) 103 | 104 | def decode(self, *args, **kwargs): 105 | return str_(self).decode(*args, **kwargs) 106 | 107 | 108 | class ColorText(object): 109 | '''String imposter that supports colored strings, mostly so len() 110 | reports the actual text's length 111 | ''' 112 | fmt = '\033[{fg:d};{bg:d};{f}m{t}\033[0m' 113 | 114 | def __init__(self, text, foreground=0, background=0, ignore_setting=False): 115 | self.text = text 116 | if isinstance(foreground, ColorCombo): 117 | self.color = foreground 118 | else: 119 | self.color = ColorCombo(foreground or FG_RESET, 120 | background or BG_RESET) 121 | self.ignore_setting = ignore_setting 122 | 123 | def __len__(self): 124 | return len(self.text) 125 | 126 | def __unicode__(self): 127 | if not _enabled or (not self.ignore_setting and not enabled): 128 | return self.text 129 | return self.fmt.format(fg=self.color.foreground, 130 | bg=self.color.background, 131 | f=self.color.flag, 132 | t=self.text) 133 | 134 | def __str__(self): 135 | if PY3: 136 | return str_(self.__unicode__()) 137 | return self.__unicode__().encode('utf8') 138 | 139 | def __repr__(self): 140 | r = ''.format(self.text, repr(self.color)) 141 | if PY3: 142 | return r 143 | return r.encode('utf8') 144 | 145 | def __add__(self, other): 146 | return ColorTextRun(self, other) 147 | 148 | def __radd__(self, other): 149 | return ColorTextRun(other, self) 150 | 151 | def encode(self, *args, **kwargs): 152 | return str_(self).encode(*args, **kwargs) 153 | 154 | def decode(self, *args, **kwargs): 155 | return str_(self).decode(*args, **kwargs) 156 | -------------------------------------------------------------------------------- /moult/compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | PY3 = sys.version_info[0] == 3 4 | 5 | str_ = str 6 | if not PY3: 7 | str_ = unicode # noqa 8 | -------------------------------------------------------------------------------- /moult/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class MoultCommandError(Exception): 3 | pass 4 | 5 | 6 | class MoultScannerError(Exception): 7 | pass 8 | -------------------------------------------------------------------------------- /moult/filesystem_scanner.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from .classes import PyModule 5 | from .ast_scanner import ast_scan_file 6 | from .frameworks import django 7 | from . import utils, log 8 | 9 | 10 | max_directory_depth = 20 11 | max_file_size = 1024 * 1204 12 | 13 | # Common ignorable directories 14 | _dir_ignore = re.compile(r'(\.(git|hg|svn|tox)|CVS|__pycache__)\b') 15 | 16 | # Files to not even bother with scanning 17 | _ext_ignore = re.compile(r'\.(pyc|html|js|css|zip|tar(\.gz)?|txt|swp|~|bak|db)$', re.I) 18 | 19 | 20 | def _scan_file(filename, sentinel, source_type='import'): 21 | '''Generator that performs the actual scanning of files. 22 | 23 | Yeilds a tuple containing import type, import path, and an extra file 24 | that should be scanned. Extra file scans should be the file or directory 25 | that relates to the import name. 26 | ''' 27 | filename = os.path.abspath(filename) 28 | real_filename = os.path.realpath(filename) 29 | 30 | if os.path.getsize(filename) <= max_file_size: 31 | if real_filename not in sentinel and os.path.isfile(filename): 32 | sentinel.add(real_filename) 33 | 34 | basename = os.path.basename(filename) 35 | scope, imports = ast_scan_file(filename) 36 | 37 | if scope is not None and imports is not None: 38 | for imp in imports: 39 | yield (source_type, imp.module, None) 40 | 41 | if 'INSTALLED_APPS' in scope and basename == 'settings.py': 42 | log.info('Found Django settings: %s', filename) 43 | for item in django.handle_django_settings(filename): 44 | yield item 45 | else: 46 | log.warn('Could not scan imports from: %s', filename) 47 | else: 48 | log.warn('File size too large: %s', filename) 49 | 50 | 51 | def _scan_directory(directory, sentinel, depth=0): 52 | '''Basically os.listdir with some filtering. 53 | ''' 54 | directory = os.path.abspath(directory) 55 | real_directory = os.path.realpath(directory) 56 | 57 | if depth < max_directory_depth and real_directory not in sentinel \ 58 | and os.path.isdir(directory): 59 | sentinel.add(real_directory) 60 | 61 | for item in os.listdir(directory): 62 | if item in ('.', '..'): 63 | # I'm not sure if this is even needed any more. 64 | continue 65 | 66 | p = os.path.abspath(os.path.join(directory, item)) 67 | if (os.path.isdir(p) and _dir_ignore.search(p)) \ 68 | or (os.path.isfile(p) and _ext_ignore.search(p)): 69 | continue 70 | 71 | yield p 72 | 73 | 74 | def scan_file(pym, filename, sentinel, installed): 75 | '''Entry point scan that creates a PyModule instance if needed. 76 | ''' 77 | if not utils.is_python_script(filename): 78 | return 79 | 80 | if not pym: 81 | # This is for finding a previously created instance, not finding an 82 | # installed module with the same name. Might need to base the name 83 | # on the actual paths to reduce ambiguity in the printed scan results. 84 | module = os.path.basename(filename) 85 | pym = utils.find_package(module, installed) 86 | if not pym: 87 | pym = PyModule(module, 'SCRIPT', os.path.abspath(filename)) 88 | installed.insert(0, pym) 89 | else: 90 | pym.is_scan = True 91 | 92 | for imp_type, import_path, extra_file_scan in _scan_file(filename, sentinel): 93 | dep = utils.find_package(import_path, installed) 94 | if dep: 95 | dep.add_dependant(pym) 96 | pym.add_dependency(dep) 97 | 98 | if imp_type != 'import': 99 | pym.add_framework(imp_type) 100 | 101 | if extra_file_scan: 102 | # extra_file_scan should be a directory or file containing the 103 | # import name 104 | scan_filename = utils.file_containing_import(import_path, extra_file_scan) 105 | log.info('Related scan: %s - %s', import_path, scan_filename) 106 | if scan_filename.endswith('__init__.py'): 107 | scan_directory(pym, os.path.dirname(scan_filename), sentinel, installed) 108 | else: 109 | scan_file(pym, scan_filename, sentinel, installed) 110 | 111 | return pym 112 | 113 | 114 | def scan_directory(pym, directory, sentinel, installed, depth=0): 115 | '''Entry point scan that creates a PyModule instance if needed. 116 | ''' 117 | if not pym: 118 | d = os.path.abspath(directory) 119 | basename = os.path.basename(d) 120 | pym = utils.find_package(basename, installed) 121 | if not pym: 122 | version = 'DIRECTORY' 123 | if os.path.isfile(os.path.join(d, '__init__.py')): 124 | version = 'MODULE' 125 | pym = PyModule(basename, version, d) 126 | installed.insert(0, pym) 127 | else: 128 | pym.is_scan = True 129 | 130 | # Keep track of how many file scans resulted in nothing 131 | bad_scans = 0 132 | 133 | for item in _scan_directory(directory, sentinel, depth): 134 | if os.path.isfile(item): 135 | if bad_scans > 100: 136 | # Keep in mind this counter resets if it a good scan happens 137 | # in *this* directory. If you have a module with more than 100 138 | # files in a single directory, you should probably refactor it. 139 | log.debug('Stopping scan of directory since it looks like a data dump: %s', directory) 140 | break 141 | 142 | if not scan_file(pym, item, sentinel, installed): 143 | bad_scans += 1 144 | else: 145 | bad_scans = 0 146 | elif os.path.isdir(item): 147 | scan_directory(pym, item, sentinel, installed, depth + 1) 148 | 149 | return pym 150 | 151 | 152 | def scan(filename, installed, sentinel=None): 153 | if not sentinel: 154 | sentinel = set() 155 | 156 | if os.path.isfile(filename): 157 | return scan_file(None, filename, sentinel, installed) 158 | elif os.path.isdir(filename): 159 | return scan_directory(None, filename, sentinel, installed) 160 | else: 161 | log.error('Could not scan: %s', filename) 162 | -------------------------------------------------------------------------------- /moult/frameworks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweekmonster/moult/38d3a3b9002336219897ebe263ca1d8dcadbecf5/moult/frameworks/__init__.py -------------------------------------------------------------------------------- /moult/frameworks/django.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import re 4 | import os 5 | import sys 6 | import time 7 | 8 | from .. import utils, log 9 | 10 | 11 | _excluded_settings = ( 12 | 'ALLOWED_HOSTS', 13 | ) 14 | 15 | _filescan_modules = ( 16 | 'django.db.backends', 17 | 'django.core.cache.backends', 18 | ) 19 | 20 | 21 | def scan_django_settings(values, imports): 22 | '''Recursively scans Django settings for values that appear to be 23 | imported modules. 24 | ''' 25 | if isinstance(values, (str, bytes)): 26 | if utils.is_import_str(values): 27 | imports.add(values) 28 | elif isinstance(values, dict): 29 | for k, v in values.items(): 30 | scan_django_settings(k, imports) 31 | scan_django_settings(v, imports) 32 | elif hasattr(values, '__file__') and getattr(values, '__file__'): 33 | imp, _ = utils.import_path_from_file(getattr(values, '__file__')) 34 | imports.add(imp) 35 | elif hasattr(values, '__iter__'): 36 | for item in values: 37 | scan_django_settings(item, imports) 38 | 39 | 40 | def handle_django_settings(filename): 41 | '''Attempts to load a Django project and get package dependencies from 42 | settings. 43 | 44 | Tested using Django 1.4 and 1.8. Not sure if some nuances are missed in 45 | the other versions. 46 | ''' 47 | old_sys_path = sys.path[:] 48 | dirpath = os.path.dirname(filename) 49 | project = os.path.basename(dirpath) 50 | cwd = os.getcwd() 51 | project_path = os.path.normpath(os.path.join(dirpath, '..')) 52 | if project_path not in sys.path: 53 | sys.path.insert(0, project_path) 54 | os.chdir(project_path) 55 | 56 | project_settings = '{}.settings'.format(project) 57 | os.environ['DJANGO_SETTINGS_MODULE'] = project_settings 58 | 59 | try: 60 | import django 61 | # Sanity 62 | django.setup = lambda: False 63 | except ImportError: 64 | log.error('Found Django settings, but Django is not installed.') 65 | return 66 | 67 | log.warn('Loading Django Settings (Using {}): {}' 68 | .format(django.get_version(), filename)) 69 | 70 | from django.conf import LazySettings 71 | 72 | installed_apps = set() 73 | settings_imports = set() 74 | 75 | try: 76 | settings = LazySettings() 77 | settings._setup() 78 | for k, v in vars(settings._wrapped).items(): 79 | if k not in _excluded_settings and re.match(r'^[A-Z_]+$', k): 80 | # log.debug('Scanning Django setting: %s', k) 81 | scan_django_settings(v, settings_imports) 82 | 83 | # Manually scan INSTALLED_APPS since the broad scan won't include 84 | # strings without a period in it . 85 | for app in getattr(settings, 'INSTALLED_APPS', []): 86 | if hasattr(app, '__file__') and getattr(app, '__file__'): 87 | imp, _ = utils.import_path_from_file(getattr(app, '__file__')) 88 | installed_apps.add(imp) 89 | else: 90 | installed_apps.add(app) 91 | except Exception as e: 92 | log.error('Could not load Django settings: %s', e) 93 | log.debug('', exc_info=True) 94 | return 95 | 96 | if not installed_apps or not settings_imports: 97 | log.error('Got empty settings values from Django settings.') 98 | 99 | try: 100 | from django.apps.registry import apps, Apps, AppRegistryNotReady 101 | # Django doesn't like it when the initial instance of `apps` is reused, 102 | # but it has to be populated before other instances can be created. 103 | if not apps.apps_ready: 104 | apps.populate(installed_apps) 105 | else: 106 | apps = Apps(installed_apps) 107 | 108 | start = time.time() 109 | while True: 110 | try: 111 | for app in apps.get_app_configs(): 112 | installed_apps.add(app.name) 113 | except AppRegistryNotReady: 114 | if time.time() - start > 10: 115 | raise Exception('Bail out of waiting for Django') 116 | log.debug('Waiting for apps to load...') 117 | continue 118 | break 119 | except Exception as e: 120 | log.debug('Could not use AppConfig: {}'.format(e)) 121 | 122 | # Restore before sub scans can occur 123 | sys.path[:] = old_sys_path 124 | os.chdir(cwd) 125 | 126 | for item in settings_imports: 127 | need_scan = item.startswith(_filescan_modules) 128 | yield ('django', item, project_path if need_scan else None) 129 | 130 | for app in installed_apps: 131 | need_scan = app.startswith(project) 132 | yield ('django', app, project_path if need_scan else None) 133 | -------------------------------------------------------------------------------- /moult/log.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, unicode_literals 2 | 3 | import sys 4 | import logging 5 | 6 | from . import color 7 | 8 | level = logging.WARNING 9 | 10 | 11 | _log = logging.getLogger('moult') 12 | _log.propagate = False 13 | 14 | _level_color = { 15 | logging.DEBUG: 0, 16 | logging.INFO: color.FG_BLUE, 17 | logging.WARN: color.FG_YELLOW, 18 | logging.ERROR: color.FG_RED, 19 | logging.FATAL: color.NOOO, 20 | } 21 | 22 | 23 | debug = _log.debug 24 | info = _log.info 25 | warn = _log.warn 26 | error = _log.error 27 | fatal = _log.fatal 28 | exception = _log.exception 29 | 30 | 31 | def set_level(level): 32 | if not level: 33 | _log.disabled = True 34 | else: 35 | _log.setLevel(level) 36 | 37 | 38 | class ColorOutputHandler(logging.Handler): 39 | def emit(self, record): 40 | msg = self.format(record) 41 | c = _level_color.get(record.levelno, 0) 42 | if record.levelno == logging.DEBUG: 43 | msg = 'DEBUG: ' + msg 44 | print(color.ColorText(msg, c), file=sys.stderr) 45 | 46 | 47 | handler = ColorOutputHandler() 48 | # handler.setFormatter(logging.Formatter('%(message)s')) 49 | _log.addHandler(handler) 50 | _log.setLevel(level) 51 | -------------------------------------------------------------------------------- /moult/pip_importer.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from .exceptions import MoultCommandError 4 | 5 | try: 6 | # pip >= 6.0 7 | from pip.utils import (dist_is_local, dist_in_usersite, 8 | get_installed_distributions, 9 | running_under_virtualenv) 10 | except ImportError: 11 | try: 12 | # pip >= 1.3 13 | from pip.util import (dist_is_local, dist_in_usersite, 14 | get_installed_distributions, 15 | running_under_virtualenv) 16 | except ImportError: 17 | raise MoultCommandError('Could not import pip functions') 18 | 19 | 20 | # More packages that most likely wouldn't be used by other packages. 21 | # They're listed here in case they weren't installed normally. 22 | ignore_packages = ( 23 | 'setuptools', 24 | 'pip', 25 | 'python', 26 | 'distribute', 27 | 'virtualenv', 28 | 'virtualenvwrapper', 29 | 'ipython', 30 | 'supervisor', 31 | ) 32 | 33 | 34 | __all__ = ('dist_is_local', 'dist_in_usersite', 'get_installed_distributions', 35 | 'running_under_virtualenv', 'ignore_packages') 36 | -------------------------------------------------------------------------------- /moult/printer.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | import sys 5 | import time 6 | 7 | from .utils import running_under_virtualenv 8 | from .color import * 9 | from .exceptions import MoultCommandError 10 | from .compat import str_ 11 | from . import __version__ 12 | 13 | 14 | __all__ = ('enable_debug', 'output', 'error', 'wrap', 'print_module') 15 | 16 | 17 | enable_debug = False 18 | tab_width = 2 19 | 20 | 21 | def output(*args, **kwargs): 22 | '''Analog of print() but with an indent option 23 | ''' 24 | indent = kwargs.pop('indent', 0) 25 | sep = kwargs.pop('sep', None) 26 | kwargs['sep'] = u'' # Sanity 27 | if sep is None: 28 | sep = u' ' 29 | indent_str = u' ' * (indent * tab_width) 30 | text = sep.join(map(str_, args)) 31 | color = kwargs.pop('color', None) 32 | if color: 33 | color.bright = kwargs.pop('bright', None) 34 | text = ColorText(text, color) 35 | print(indent_str + text, **kwargs) 36 | 37 | 38 | def error(message, fatal=False): 39 | if fatal: 40 | raise MoultCommandError(message) 41 | output(ColorText(message, MEH), file=sys.stderr) 42 | 43 | 44 | def wrap(items, prefix=0, width=80): 45 | width -= prefix 46 | 47 | lines = [] 48 | line_i = 0 49 | line = ColorTextRun() 50 | 51 | for i, item in enumerate(items): 52 | if i and (line_i - 1 >= width or line_i + len(item) + 1 >= width): 53 | line_i = 0 54 | if len(lines): 55 | indent = u' ' * prefix 56 | else: 57 | indent = u'' 58 | lines.append(str_(indent + line).rstrip()) 59 | line = ColorTextRun() 60 | 61 | line += item + ', ' 62 | line_i += 2 + len(item) 63 | 64 | if line: 65 | if len(lines): 66 | indent = u' ' * prefix 67 | else: 68 | indent = u'' 69 | lines.append(str_(indent + line).rstrip()) 70 | 71 | return u'\n'.join(lines).rstrip(', ') 72 | 73 | 74 | def file_string(filename): 75 | return ColorTextRun(os.path.dirname(filename), 76 | os.path.sep, 77 | ColorText(os.path.basename(filename), HEY)) 78 | 79 | 80 | def module_string(pym, require=False, plain=False): 81 | s = pym.name 82 | c = NEAT 83 | 84 | if pym.is_scan: 85 | c = MEH 86 | elif pym.hidden: 87 | c = SHHH 88 | elif pym.local and running_under_virtualenv(): 89 | c = GOOD 90 | elif not pym.local and running_under_virtualenv(): 91 | c = HEY 92 | 93 | s = ColorText(s, c) 94 | 95 | if pym.hidden and plain: 96 | s = ColorText(u'_', HEY) + s 97 | 98 | if plain: 99 | return s 100 | 101 | if require: 102 | s += ColorTextRun(u'==', ColorText(pym.version, NEAT)) 103 | else: 104 | s += ColorTextRun(u' (', ColorText(pym.version, NEAT), u')') 105 | 106 | return s 107 | 108 | 109 | def require_string(pym): 110 | return module_string(pym, require=True) 111 | 112 | 113 | def print_requires(pkg, show_all=False, printed=None): 114 | if printed is None: 115 | printed = set() 116 | 117 | for dep in pkg.dependencies: 118 | if not dep.dependants: 119 | print_requires(dep, show_all=show_all, printed=printed) 120 | 121 | for dep in pkg.dependencies: 122 | print_requires(dep, show_all=show_all, printed=printed) 123 | 124 | if pkg in printed: 125 | return 126 | 127 | printed.add(pkg) 128 | if not pkg.is_scan: 129 | if pkg.missing: 130 | output('#', end=' ') 131 | output(require_string(pkg)) 132 | 133 | 134 | def print_frozen(scans, show_all=False, printed=None): 135 | output('# Requirements based on scans from:') 136 | for pym in scans: 137 | output('# {}'.format(pym.location)) 138 | date_str = time.strftime('%Y-%m-%d %H:%M:%S %Z', time.localtime()) 139 | output('# Generated with moult {} at {}'.format(__version__, date_str)) 140 | 141 | printed = set() 142 | for scan in scans: 143 | print_requires(scan, show_all=show_all, printed=printed) 144 | 145 | 146 | def print_module(pym, depth=0, indent_str=' ', printed=None, detail=False, 147 | show_dependants=False, show_dependencies=False): 148 | if not printed: 149 | printed = [] 150 | 151 | if pym in printed: 152 | return 153 | 154 | printed.append(pym) 155 | 156 | output(module_string(pym, not detail), indent=depth) 157 | 158 | if detail: 159 | loc = u'NOT INSTALLED' 160 | if pym.is_scan: 161 | loc = pym.location 162 | elif pym.local and running_under_virtualenv(): 163 | loc = ColorText(u'VirtualEnv', YAY) 164 | elif pym.user: 165 | loc = ColorText(u'User', NEAT) 166 | elif not pym.missing: 167 | c = HEY.copy() 168 | c.set_bright(True) 169 | loc = ColorText(u'System', c) 170 | 171 | rows = [(u'Location:', [loc])] 172 | 173 | notes = [] 174 | 175 | if pym.hidden: 176 | notes.append(ColorText(u'Hidden Package', SHHH)) 177 | 178 | if 'django' in pym.frameworks: 179 | notes.append(ColorText(u'Contains Django project', NEAT)) 180 | 181 | if notes: 182 | rows.append((u'Notes:', notes)) 183 | 184 | if pym.installed_scripts: 185 | rows.append((u'Scripts:', 186 | [file_string(x) for x in pym.installed_scripts])) 187 | 188 | if pym.installed_files: 189 | rows.append((u'Files:', 190 | [file_string(x) for x in pym.installed_files])) 191 | 192 | if pym.dependants: 193 | rows.append((u'Used In:', 194 | [module_string(x) for x in pym.dependants])) 195 | 196 | if not pym.dependencies: 197 | items = [ColorText(u'None', NEAT)] 198 | else: 199 | items = [module_string(x) for x in pym.dependencies] 200 | rows.append((u'Requires:', items)) 201 | 202 | tab = max(map(lambda x: len(x[0]), rows)) 203 | for label, items in rows: 204 | # Label width, tab width, and space 205 | w_tab = tab + ((depth + 1) * tab_width) + 1 206 | output(label.rjust(tab), wrap(items, w_tab), indent=depth + 1) 207 | 208 | print('') 209 | 210 | if show_dependants and pym.dependants: 211 | for dep in pym.dependants: 212 | print_module(dep, depth, detail=detail, printed=printed, 213 | show_dependencies=show_dependencies) 214 | 215 | if show_dependencies and pym.dependencies: 216 | for dep in pym.dependencies: 217 | print_module(dep, depth, detail=detail, printed=printed, 218 | show_dependencies=show_dependencies) 219 | -------------------------------------------------------------------------------- /moult/program.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from .args import create_argparser 4 | from .exceptions import MoultCommandError 5 | from . import color, printer, filesystem_scanner, utils, log 6 | 7 | 8 | def more_turtles(packages, show_all=False): 9 | remove = [] 10 | for pym in packages: 11 | if not show_all and pym.hidden: 12 | continue 13 | if not pym.missing and not pym.is_scan and not len(pym._dependants): 14 | remove.append(pym) 15 | 16 | return remove 17 | 18 | 19 | def moult(packages=None, detail=False, scan=None, local=False, recursive=False, 20 | plain=False, show_all=False, freeze=False, **kwargs): 21 | installed = utils.installed_packages(local=local) 22 | 23 | if packages is None: 24 | packages = [] 25 | 26 | if freeze and not scan: 27 | scan = ['.'] 28 | 29 | if scan: 30 | header_printed = False 31 | 32 | for d in scan: 33 | pym = filesystem_scanner.scan(d, installed) 34 | 35 | if not freeze and pym: 36 | if not header_printed: 37 | printer.output('Found in scan:', color=color.YAY) 38 | header_printed = True 39 | printer.print_module(pym, detail=True, depth=1) 40 | 41 | if freeze: 42 | scans = [s for s in installed if s.is_scan] 43 | printer.print_frozen(scans, show_all=show_all) 44 | return 45 | 46 | displaying = [] 47 | if packages: 48 | for pkg in packages: 49 | pym = utils.find_package(pkg, installed, True) 50 | if pym: 51 | displaying.append(pym) 52 | else: 53 | import_parts = pkg.split('.') 54 | for i in range(len(import_parts), 0, -1): 55 | pym = utils.find_package('.'.join(import_parts[:i]), installed) 56 | if pym: 57 | displaying.append(pym) 58 | break 59 | 60 | if not displaying: 61 | log.error('No matching packages: %s', ', '.join(packages)) 62 | return 63 | 64 | if not displaying: 65 | displaying = installed[:] 66 | else: 67 | printer.output('Matched modules:', color=color.YAY) 68 | for pym in displaying: 69 | printer.print_module(pym, detail=True, depth=1) 70 | print('') 71 | 72 | removable = more_turtles(displaying, show_all) 73 | if not removable: 74 | printer.output('Nothing to remove', color=color.YAY, end='\n\n') 75 | return 76 | 77 | if removable: 78 | i = 0 79 | 80 | while len(removable): 81 | if not i: 82 | printer.output('Packages that can be removed:', color=color.YAY) 83 | else: 84 | printer.output('Then you could remove:', color=color.YAY) 85 | 86 | next_packages = [] 87 | 88 | for pym in removable: 89 | if plain: 90 | print(printer.module_string(pym, plain=True), end=' ') 91 | else: 92 | printer.print_module(pym, detail=detail, depth=1) 93 | 94 | for dep in pym.dependencies: 95 | dep.remove_dependant(pym) 96 | if dep not in next_packages: 97 | next_packages.append(dep) 98 | 99 | if plain: 100 | print('\n') 101 | else: 102 | print('') 103 | 104 | if not recursive: 105 | break 106 | 107 | removable = more_turtles(next_packages, show_all) 108 | i += 1 109 | 110 | 111 | def run(): 112 | parser = create_argparser() 113 | args = parser.parse_args() 114 | 115 | if args.no_color or args.no_colour: 116 | color.enabled = False 117 | 118 | if args.verbose: 119 | log.set_level(log.level - min(args.verbose, 2) * 10) 120 | 121 | if args.freeze: 122 | log.set_level(0) 123 | color.enabled = False 124 | 125 | exit_code = 0 126 | 127 | try: 128 | moult(**vars(args)) 129 | except MoultCommandError as e: 130 | exit_code = 1 131 | log.fatal('Error: %s', e) 132 | except KeyboardInterrupt: 133 | exit_code = 1 134 | import getpass 135 | print('\n{}, eat a snickers'.format(getpass.getuser())) 136 | finally: 137 | if not utils.running_under_virtualenv(): 138 | printer.output('/!\\ You are not in a Virtual Environment /!\\', 139 | color=color.MAN) 140 | 141 | return exit_code 142 | -------------------------------------------------------------------------------- /moult/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | 5 | from .classes import PyModule 6 | from .pip_importer import * 7 | from .compat import str_ 8 | 9 | _stdlib = set() 10 | _import_paths = [] 11 | 12 | 13 | __all__ = ('dist_is_local', 'dist_in_usersite', 'get_installed_distributions', 14 | 'running_under_virtualenv', 'ignore_packages', 15 | 'search_packages_info', 'find_package') 16 | 17 | 18 | def load_stdlib(): 19 | '''Scans sys.path for standard library modules. 20 | ''' 21 | if _stdlib: 22 | return _stdlib 23 | 24 | prefixes = tuple({os.path.abspath(p) for p in ( 25 | sys.prefix, 26 | getattr(sys, 'real_prefix', sys.prefix), 27 | getattr(sys, 'base_prefix', sys.prefix), 28 | )}) 29 | 30 | for sp in sys.path: 31 | if not sp: 32 | continue 33 | _import_paths.append(os.path.abspath(sp)) 34 | 35 | stdpaths = tuple({p for p in _import_paths 36 | if p.startswith(prefixes) and 'site-packages' not in p}) 37 | 38 | _stdlib.update(sys.builtin_module_names) 39 | 40 | for stdpath in stdpaths: 41 | if not os.path.isdir(stdpath): 42 | continue 43 | 44 | for item in os.listdir(stdpath): 45 | if item.startswith('.') or item == 'site-packages': 46 | continue 47 | 48 | p = os.path.join(stdpath, item) 49 | if not os.path.isdir(p) and not item.endswith(('.py', '.so')): 50 | continue 51 | 52 | _stdlib.add(item.split('.', 1)[0]) 53 | 54 | return _stdlib 55 | 56 | 57 | load_stdlib() 58 | 59 | 60 | def is_stdlib(module): 61 | return module.split('.', 1)[0] in load_stdlib() 62 | 63 | 64 | def is_import_str(text): 65 | text = str_(text) 66 | return re.match(r'^[\w\.]+$', text) and re.match(r'\w+\.\w+', text) 67 | 68 | 69 | def import_path_from_file(filename, as_list=False): 70 | '''Returns a tuple of the import path and root module directory for the 71 | supplied file. 72 | ''' 73 | module_path = [] 74 | basename = os.path.splitext(os.path.basename(filename))[0] 75 | if basename != '__init__': 76 | module_path.append(basename) 77 | 78 | dirname = os.path.dirname(filename) 79 | while os.path.isfile(os.path.join(dirname, '__init__.py')): 80 | dirname, tail = os.path.split(dirname) 81 | module_path.insert(0, tail) 82 | 83 | if as_list: 84 | return module_path, dirname 85 | return '.'.join(module_path), dirname 86 | 87 | 88 | def file_containing_import(import_path, import_root): 89 | '''Finds the file that might contain the import_path. 90 | ''' 91 | if not _import_paths: 92 | load_stdlib() 93 | 94 | if os.path.isfile(import_root): 95 | import_root = os.path.dirname(import_root) 96 | 97 | search_paths = [import_root] + _import_paths 98 | module_parts = import_path.split('.') 99 | for i in range(len(module_parts), 0, -1): 100 | module_path = os.path.join(*module_parts[:i]) 101 | for sp in search_paths: 102 | p = os.path.join(sp, module_path) 103 | if os.path.isdir(p): 104 | return os.path.join(p, '__init__.py') 105 | elif os.path.isfile(p + '.py'): 106 | return p + '.py' 107 | return None 108 | 109 | 110 | def resolve_import(import_path, from_module): 111 | '''Resolves relative imports from a module. 112 | ''' 113 | if not import_path or not import_path.startswith('.'): 114 | return import_path 115 | 116 | from_module = from_module.split('.') 117 | dots = 0 118 | for c in import_path: 119 | if c == '.': 120 | dots += 1 121 | else: 122 | break 123 | 124 | if dots: 125 | from_module = from_module[:-dots] 126 | import_path = import_path[dots:] 127 | 128 | if import_path: 129 | from_module.append(import_path) 130 | 131 | return '.'.join(from_module) 132 | 133 | 134 | def find_package(name, installed, package=False): 135 | '''Finds a package in the installed list. 136 | 137 | If `package` is true, match package names, otherwise, match import paths. 138 | ''' 139 | if package: 140 | name = name.lower() 141 | tests = ( 142 | lambda x: x.user and name == x.name.lower(), 143 | lambda x: x.local and name == x.name.lower(), 144 | lambda x: name == x.name.lower(), 145 | ) 146 | else: 147 | tests = ( 148 | lambda x: x.user and name in x.import_names, 149 | lambda x: x.local and name in x.import_names, 150 | lambda x: name in x.import_names, 151 | ) 152 | 153 | for t in tests: 154 | try: 155 | found = list(filter(t, installed)) 156 | if found and not found[0].is_scan: 157 | return found[0] 158 | except StopIteration: 159 | pass 160 | return None 161 | 162 | 163 | def is_script(filename): 164 | '''Checks if a file has a hashbang. 165 | ''' 166 | if not os.path.isfile(filename): 167 | return False 168 | 169 | try: 170 | with open(filename, 'rb') as fp: 171 | return fp.read(2) == b'#!' 172 | except IOError: 173 | pass 174 | 175 | return False 176 | 177 | 178 | def is_python_script(filename): 179 | '''Checks a file to see if it's a python script of some sort. 180 | ''' 181 | if filename.lower().endswith('.py'): 182 | return True 183 | 184 | if not os.path.isfile(filename): 185 | return False 186 | 187 | try: 188 | with open(filename, 'rb') as fp: 189 | if fp.read(2) != b'#!': 190 | return False 191 | return re.match(r'.*python', str_(fp.readline())) 192 | except IOError: 193 | pass 194 | 195 | return False 196 | 197 | 198 | def iter_dist_files(dist): 199 | if dist.has_metadata('RECORD'): 200 | for line in dist.get_metadata_lines('RECORD'): 201 | line = line.split(',')[0] 202 | if line.endswith('.pyc'): 203 | continue 204 | yield os.path.normpath(os.path.join(dist.location, line)) 205 | elif dist.has_metadata('installed-files.txt'): 206 | for line in dist.get_metadata_lines('installed-files.txt'): 207 | if line.endswith('.pyc'): 208 | continue 209 | yield os.path.normpath(os.path.join(dist.location, 210 | dist.egg_info, line)) 211 | 212 | 213 | def installed_packages(local=False): 214 | installed = [] 215 | 216 | for dist in get_installed_distributions(local_only=local): 217 | pym = PyModule(dist.project_name, dist.version, dist.location) 218 | if dist.has_metadata('top_level.txt'): 219 | pym.set_import_names(list(dist.get_metadata_lines('top_level.txt'))) 220 | 221 | pym.local = dist_is_local(dist) 222 | pym.user = dist_in_usersite(dist) 223 | pym._dependencies = [dep.project_name for dep in dist.requires()] 224 | 225 | for filename in iter_dist_files(dist): 226 | if not filename.startswith(dist.location): 227 | if is_script(filename): 228 | pym.installed_scripts.append(filename) 229 | else: 230 | pym.installed_files.append(filename) 231 | 232 | if pym.installed_scripts or pym.name in ignore_packages: 233 | pym.hidden = True 234 | 235 | installed.append(pym) 236 | 237 | for pym in installed[:]: 238 | for dep in pym._dependencies: 239 | if dep == 'argparse': 240 | # Since I'm only testing with Python 2.7, skip any requirements 241 | # for argparse. 242 | continue 243 | pymc = find_package(dep, installed, True) 244 | if not pymc: 245 | pymc = PyModule(dep, 'MISSING', missing=True) 246 | installed.append(pymc) 247 | pymc.add_dependant(pym) 248 | pym.add_dependency(pymc) 249 | 250 | return installed 251 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweekmonster/moult/38d3a3b9002336219897ebe263ca1d8dcadbecf5/setup.cfg -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from setuptools import setup, find_packages 4 | from setuptools.command.test import test as TestCommand 5 | 6 | try: 7 | from pypandoc import convert 8 | 9 | def readme_file(readme): 10 | return convert(readme, 'rst') 11 | except ImportError: 12 | def readme_file(readme): 13 | with open(readme, 'r') as fp: 14 | return fp.read() 15 | 16 | moult = __import__('moult') 17 | 18 | description = moult.__doc__.strip() 19 | 20 | 21 | class Tox(TestCommand): 22 | user_options = [('tox-args=', 'a', "Arguments to pass to tox")] 23 | 24 | def initialize_options(self): 25 | TestCommand.initialize_options(self) 26 | self.tox_args = None 27 | 28 | def finalize_options(self): 29 | TestCommand.finalize_options(self) 30 | self.test_args = [] 31 | self.test_suite = True 32 | 33 | def run_tests(self): 34 | import tox 35 | import shlex 36 | 37 | args = self.tox_args 38 | if args: 39 | args = shlex.split(self.tox_args) 40 | errno = tox.cmdline(args=args) 41 | sys.exit(errno) 42 | 43 | 44 | setup( 45 | name='moult', 46 | author='Tommy Allen', 47 | author_email='tommy@esdf.io', 48 | version=moult.__version__, 49 | description=description, 50 | long_description=readme_file('README.md'), 51 | packages=find_packages(), 52 | url='https://github.com/tweekmonster/moult', 53 | install_requires=[], 54 | entry_points={ 55 | 'console_scripts': [ 56 | 'moult=moult:main', 57 | 'moult%s=moult:main' % sys.version[:1], 58 | 'moult%s=moult:main' % sys.version[:3], 59 | ], 60 | }, 61 | keywords='uninstall remove packages development environment requirements', 62 | license='MIT', 63 | classifiers=[ 64 | 'Development Status :: 4 - Beta', 65 | 'Intended Audience :: Developers', 66 | 'License :: OSI Approved :: MIT License', 67 | 'Programming Language :: Python :: 2', 68 | 'Programming Language :: Python :: 2.7', 69 | 'Programming Language :: Python :: 3', 70 | 'Programming Language :: Python :: 3.2', 71 | 'Programming Language :: Python :: 3.3', 72 | 'Programming Language :: Python :: 3.4', 73 | 'Programming Language :: Python :: Implementation :: PyPy', 74 | ], 75 | 76 | tests_require=['tox'], 77 | cmdclass={ 78 | 'test': Tox, 79 | } 80 | ) 81 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweekmonster/moult/38d3a3b9002336219897ebe263ca1d8dcadbecf5/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import copy 3 | import pytest 4 | import logging 5 | 6 | 7 | from moult import log 8 | from moult.utils import installed_packages 9 | 10 | from py._path.local import LocalPath 11 | 12 | 13 | log.set_level(logging.DEBUG) 14 | 15 | 16 | class ScriptData(object): 17 | def __init__(self, tmpdir): 18 | self.tmpdir = tmpdir 19 | self.tmpdata = None 20 | self.pristine_data = LocalPath(os.path.dirname(__file__)).join('data') 21 | self.installed_packages = None 22 | 23 | def copy_data(self): 24 | if self.tmpdata and self.tmpdata.exists(): 25 | self.tmpdata.remove(ignore_errors=True) 26 | self.tmpdata = self.tmpdir.mkdir('data') 27 | self.pristine_data.copy(self.tmpdata, mode=True) 28 | 29 | # Can't add .git directories to the index 30 | git_no_scan = self.pristine_data.join('scripts/project/.hg') 31 | hg_no_scan = self.tmpdata.join('scripts/project/.git') 32 | git_no_scan.copy(hg_no_scan) 33 | 34 | return self.tmpdata 35 | 36 | def sysexec(self, script): 37 | print('Executing Script: %s' % script) 38 | return script.sysexec(cwd=str(script.dirpath())) 39 | 40 | def verify_data(self): 41 | if not self.tmpdata: 42 | return False 43 | 44 | for prissy in self.pristine_data.visit(): 45 | assert prissy.ext != '.pyc', \ 46 | 'Pristine has Python bytecode indicating execution from pristine directory!' 47 | 48 | rel = prissy.relto(self.pristine_data) 49 | tmp = self.tmpdata.join(rel) 50 | 51 | if prissy.check(dir=True): 52 | assert tmp.check(dir=True), 'Data integirty test failed: %s' % rel 53 | elif prissy.check(file=True): 54 | assert tmp.check(file=True), 'Data integirty test failed: %s' % rel 55 | assert prissy.computehash() == tmp.computehash(), 'Hash mismatch: %s' % rel 56 | 57 | for tmp in self.tmpdata.visit(): 58 | if '.git' in tmp.strpath or '__pycache__' in tmp.strpath or tmp.ext == '.pyc': 59 | continue 60 | 61 | rel = tmp.relto(self.tmpdata) 62 | prissy = self.pristine_data.join(rel) 63 | 64 | if tmp.check(dir=True): 65 | assert prissy.check(dir=True), 'Directory created in tmpdir: %s' % rel 66 | elif tmp.check(file=True): 67 | assert prissy.check(file=True), 'File created in tmpdir: %s' % rel 68 | 69 | return True 70 | 71 | def copy_installed(self): 72 | if not self.installed_packages: 73 | self.installed_packages = installed_packages() 74 | return copy.deepcopy(self.installed_packages) 75 | 76 | 77 | @pytest.fixture 78 | def data(tmpdir): 79 | return ScriptData(tmpdir) 80 | -------------------------------------------------------------------------------- /tests/data/scripts/loose/bad_tabs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # This script should still be scannable. Moult is not the syntax police. 4 | import os, moult 5 | import ( 6 | os, moult, 7 | testpackage 8 | ) 9 | import sys 10 | from moult import __version__ 11 | 12 | from moult.utils import 13 | ( ham, 14 | spam, eggs, 15 | 16 | cheese as Red_Leicester , 17 | bacon, 18 | ) 19 | 20 | sys.stdout.write('Moult Version: %s' % __version__) 21 | 22 | # This script should never execute when scanned 23 | directory = '../readonly' 24 | for filename in os.listdir(directory): 25 | os.remove(os.path.join(directory, filename)) 26 | -------------------------------------------------------------------------------- /tests/data/scripts/loose/obvious_script.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # This script is obvious because it ends with .py 3 | import os 4 | import sys 5 | from moult import __version__ 6 | 7 | sys.stdout.write('Moult Version: %s' % __version__) 8 | 9 | # This script should never execute when scanned 10 | directory = '../readonly' 11 | for filename in os.listdir(directory): 12 | os.remove(os.path.join(directory, filename)) 13 | -------------------------------------------------------------------------------- /tests/data/scripts/loose/unicode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Note there is no coding 3 | # こんにちは! 4 | import os 5 | import sys 6 | from moult import __version__ 7 | 8 | sys.stdout.write('脱皮バージョン: %s' % __version__) 9 | 10 | # This script should never execute when scanned 11 | directory = '../readonly' 12 | for filename in os.listdir(directory): 13 | os.remove(os.path.join(directory, filename)) 14 | -------------------------------------------------------------------------------- /tests/data/scripts/loose/unicode_import.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Note there is no coding 3 | # こんにちは! 4 | import os 5 | import sys 6 | from moult import __version__ 7 | from 脱皮 import それが壊れていました 8 | 9 | sys.stdout.write('脱皮バージョン: %s' % __version__) 10 | 11 | # This script should never execute when scanned 12 | directory = '../readonly' 13 | for filename in os.listdir(directory): 14 | os.remove(os.path.join(directory, filename)) 15 | -------------------------------------------------------------------------------- /tests/data/scripts/project/.hg/dont_scan_this.py: -------------------------------------------------------------------------------- 1 | from fake_package import spam 2 | 3 | -------------------------------------------------------------------------------- /tests/data/scripts/project/.svn/dont_scan_this.py: -------------------------------------------------------------------------------- 1 | from fake_package import spam 2 | 3 | -------------------------------------------------------------------------------- /tests/data/scripts/project/.tox/dont_scan_this.py: -------------------------------------------------------------------------------- 1 | from fake_package import spam 2 | 3 | -------------------------------------------------------------------------------- /tests/data/scripts/project/CVS/dont_scan_this.py: -------------------------------------------------------------------------------- 1 | from fake_package import spam 2 | 3 | -------------------------------------------------------------------------------- /tests/data/scripts/project/__pycache__/dont_scan_this.py: -------------------------------------------------------------------------------- 1 | from fake_package import spam 2 | 3 | -------------------------------------------------------------------------------- /tests/data/scripts/project/django_project/README: -------------------------------------------------------------------------------- 1 | This is not a functional Django project. It will not run without errors. 2 | We only want to test scanning the settings.py file! 3 | -------------------------------------------------------------------------------- /tests/data/scripts/project/django_project/django_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweekmonster/moult/38d3a3b9002336219897ebe263ca1d8dcadbecf5/tests/data/scripts/project/django_project/django_project/__init__.py -------------------------------------------------------------------------------- /tests/data/scripts/project/django_project/django_project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_project project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.8.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.8/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | import os 15 | 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'aeacnfn98d0b^iqprc#9*9ci7!l298h!_tf^x^$az7kpc^(m16' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = ( 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | # Moult should pick these up 42 | 'django_boto', 43 | 'allauth', 44 | 'testapp', 45 | ) 46 | 47 | MIDDLEWARE_CLASSES = ( 48 | 'django.contrib.sessions.middleware.SessionMiddleware', 49 | 'django.middleware.common.CommonMiddleware', 50 | 'django.middleware.csrf.CsrfViewMiddleware', 51 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 52 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | 'django.middleware.security.SecurityMiddleware', 56 | ) 57 | 58 | ROOT_URLCONF = 'django_project.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'django_project.wsgi.application' 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'testapp.custom_db', 85 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 86 | } 87 | } 88 | 89 | 90 | # Internationalization 91 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 92 | 93 | LANGUAGE_CODE = 'en-us' 94 | 95 | TIME_ZONE = 'UTC' 96 | 97 | USE_I18N = True 98 | 99 | USE_L10N = True 100 | 101 | USE_TZ = True 102 | 103 | 104 | # Static files (CSS, JavaScript, Images) 105 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 106 | 107 | STATIC_URL = '/static/' 108 | -------------------------------------------------------------------------------- /tests/data/scripts/project/django_project/django_project/urls.py: -------------------------------------------------------------------------------- 1 | """django_project URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.8/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Add an import: from blog import urls as blog_urls 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) 15 | """ 16 | from django.conf.urls import include, url 17 | from django.contrib import admin 18 | 19 | urlpatterns = [ 20 | url(r'^admin/', include(admin.site.urls)), 21 | ] 22 | -------------------------------------------------------------------------------- /tests/data/scripts/project/django_project/django_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_project.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /tests/data/scripts/project/django_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_project.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /tests/data/scripts/project/django_project/testapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweekmonster/moult/38d3a3b9002336219897ebe263ca1d8dcadbecf5/tests/data/scripts/project/django_project/testapp/__init__.py -------------------------------------------------------------------------------- /tests/data/scripts/project/django_project/testapp/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /tests/data/scripts/project/django_project/testapp/custom_db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweekmonster/moult/38d3a3b9002336219897ebe263ca1d8dcadbecf5/tests/data/scripts/project/django_project/testapp/custom_db/__init__.py -------------------------------------------------------------------------------- /tests/data/scripts/project/django_project/testapp/custom_db/base.py: -------------------------------------------------------------------------------- 1 | import testpackage 2 | 3 | 4 | def hope_you_didnt_want_store_data(): 5 | pass 6 | -------------------------------------------------------------------------------- /tests/data/scripts/project/django_project/testapp/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweekmonster/moult/38d3a3b9002336219897ebe263ca1d8dcadbecf5/tests/data/scripts/project/django_project/testapp/management/__init__.py -------------------------------------------------------------------------------- /tests/data/scripts/project/django_project/testapp/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweekmonster/moult/38d3a3b9002336219897ebe263ca1d8dcadbecf5/tests/data/scripts/project/django_project/testapp/management/commands/__init__.py -------------------------------------------------------------------------------- /tests/data/scripts/project/django_project/testapp/management/commands/s3.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from boto.s3.storage import S3Storage 4 | 5 | 6 | class Command(BaseCommand): 7 | def handle(self, *args, **kwargs): 8 | s3 = S3Storage(bucket='another-bucket', key='another-key', 9 | secret='another-secret', location='EU') 10 | # ... 11 | print('Done. I guess.') 12 | -------------------------------------------------------------------------------- /tests/data/scripts/project/django_project/testapp/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweekmonster/moult/38d3a3b9002336219897ebe263ca1d8dcadbecf5/tests/data/scripts/project/django_project/testapp/migrations/__init__.py -------------------------------------------------------------------------------- /tests/data/scripts/project/django_project/testapp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from mptt.models import MPTTModel, TreeForeignKey 3 | from django_boto.s3.storage import S3Storage 4 | 5 | # mptt was not added to INSTALLED_APPS on purpose. It should still be picked up by moult. 6 | # Code was taken from the package's examples. 7 | 8 | 9 | s3 = S3Storage() 10 | 11 | 12 | class Car(models.Model): 13 | photo = models.ImageField(storage=s3) 14 | 15 | 16 | class Genre(MPTTModel): 17 | name = models.CharField(max_length=50, unique=True) 18 | parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True) 19 | 20 | class MPTTMeta: 21 | order_insertion_by = ['name'] 22 | -------------------------------------------------------------------------------- /tests/data/scripts/project/django_project/testapp/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /tests/data/scripts/project/django_project/testapp/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /tests/data/scripts/project/nested/scripts/testmodule/__init__.py: -------------------------------------------------------------------------------- 1 | '''A deeply nested module that should be picked up in a scan. 2 | ''' 3 | -------------------------------------------------------------------------------- /tests/data/scripts/project/nested/scripts/testmodule/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweekmonster/moult/38d3a3b9002336219897ebe263ca1d8dcadbecf5/tests/data/scripts/project/nested/scripts/testmodule/utils/__init__.py -------------------------------------------------------------------------------- /tests/data/scripts/project/nested/scripts/testmodule/utils/spam.py: -------------------------------------------------------------------------------- 1 | # Import some things that is known to be in the test env 2 | import moult 3 | import setuptools 4 | import testpackage 5 | -------------------------------------------------------------------------------- /tests/data/scripts/readonly/readonly1.txt: -------------------------------------------------------------------------------- 1 | This file should remain untouched! 2 | -------------------------------------------------------------------------------- /tests/data/scripts/readonly/readonly2.txt: -------------------------------------------------------------------------------- 1 | This file should remain untouched! 2 | -------------------------------------------------------------------------------- /tests/data/scripts/readonly/readonly3.txt: -------------------------------------------------------------------------------- 1 | This file should remain untouched! 2 | -------------------------------------------------------------------------------- /tests/data/scripts/shell/bash_script: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Hi" 4 | 5 | # This script should never execute when scanned 6 | rm ../readonly/readonly1.txt 7 | rm ../readonly/readonly2.txt 8 | rm ../readonly/readonly3.txt 9 | -------------------------------------------------------------------------------- /tests/data/scripts/shell/python_script: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from moult import __version__ 5 | 6 | sys.stdout.write('Moult Version: %s' % __version__) 7 | 8 | # This script should never execute when scanned 9 | directory = '../readonly' 10 | for filename in os.listdir(directory): 11 | os.remove(os.path.join(directory, filename)) 12 | -------------------------------------------------------------------------------- /tests/test_classes.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | from __future__ import unicode_literals 3 | 4 | from moult.classes import PyModule 5 | from moult.color import ColorCombo, ColorText, ColorTextRun 6 | 7 | 8 | def test_repr(): 9 | c1 = ColorCombo(5) 10 | c2 = ColorText('हरामी', c1) 11 | c3 = ColorTextRun('हरामी', c2) 12 | c4 = PyModule('हरामी', 'test', 'test') 13 | 14 | repr(c1) 15 | repr(c2) 16 | repr(c3) 17 | repr(c4) 18 | -------------------------------------------------------------------------------- /tests/test_scanning.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | import sys 3 | 4 | import pytest 5 | 6 | from moult import filesystem_scanner, utils 7 | from moult.classes import PyModule 8 | 9 | 10 | def test_data_integrity_check(data): 11 | data_dir = data.copy_data() 12 | py_script = data_dir.join('scripts/shell/python_script') 13 | 14 | assert data.sysexec(py_script) 15 | 16 | with pytest.raises(AssertionError): 17 | data.verify_data() 18 | 19 | data_dir = data.copy_data() 20 | bash_script = data_dir.join('scripts/shell/python_script') 21 | 22 | assert data.sysexec(bash_script) 23 | 24 | with pytest.raises(AssertionError): 25 | data.verify_data() 26 | 27 | 28 | def test_scan_all(data): 29 | installed = data.copy_installed() 30 | data_dir = data.copy_data() 31 | 32 | # Fake package that's imported from a script that shouldn't be scanned 33 | fakepkg = PyModule('fake_package', 'FAKE', '/a/fake/location') 34 | fakepkg.set_import_names(['fake_package', 'fake_package.spam']) 35 | installed.append(fakepkg) 36 | 37 | pkg = filesystem_scanner.scan(str(data_dir), installed) 38 | 39 | assert pkg.version == 'DIRECTORY' 40 | assert fakepkg not in pkg.dependencies, 'Fake package should not be a dependency' 41 | 42 | packages = ('moult', 'django', 'testpackage', 'boto', 'setuptools', 43 | 'django-boto', 'django-allauth', 'django-mptt') 44 | 45 | for p in packages: 46 | dep = utils.find_package(p, installed, True) 47 | assert isinstance(dep, PyModule) 48 | assert dep in pkg.dependencies 49 | 50 | 51 | def test_nested_directory(data): 52 | installed = data.copy_installed() 53 | data_dir = data.copy_data() 54 | nested = str(data_dir.join('scripts/project/nested')) 55 | pkg = filesystem_scanner.scan(nested, installed) 56 | assert pkg in installed 57 | assert pkg.location == nested 58 | assert pkg.name == 'nested' 59 | assert pkg.version == 'DIRECTORY' 60 | assert not pkg.frameworks 61 | 62 | packages = ('moult', 'setuptools', 'testpackage') 63 | for p in packages: 64 | dep = utils.find_package(p, installed, True) 65 | assert dep in pkg.dependencies 66 | 67 | 68 | def test_scan_nested_file(data): 69 | installed = data.copy_installed() 70 | data_dir = data.copy_data() 71 | filename = str(data_dir.join('scripts/project/nested/scripts/testmodule/utils/spam.py')) 72 | 73 | pkg = filesystem_scanner.scan_file(None, filename, set(), installed) 74 | assert pkg in installed 75 | assert pkg.location == filename 76 | assert pkg.name == 'spam.py' 77 | assert pkg.version == 'SCRIPT' 78 | assert not pkg.frameworks 79 | 80 | assert data.verify_data() 81 | 82 | 83 | def test_scan_django(data): 84 | installed = data.copy_installed() 85 | data_dir = data.copy_data() 86 | django_project = str(data_dir.join('scripts/project/django_project')) 87 | pkg = filesystem_scanner.scan(django_project, installed) 88 | assert pkg in installed 89 | assert pkg.location == django_project 90 | assert pkg.name == 'django_project' 91 | assert pkg.version == 'DIRECTORY' 92 | assert 'django' in pkg.frameworks 93 | packages = ('django', 'testpackage', 'django-allauth', 'django-boto', 'django-mptt', 'boto') 94 | for p in packages: 95 | dep = utils.find_package(p, installed, True) 96 | assert dep in pkg.dependencies 97 | 98 | data.verify_data() 99 | 100 | 101 | def test_scan_module(data): 102 | installed = data.copy_installed() 103 | data_dir = data.copy_data() 104 | module = data_dir.join('scripts/project/nested/scripts/testmodule') 105 | pkg = filesystem_scanner.scan(str(module), installed) 106 | 107 | assert pkg in installed 108 | assert pkg.name == 'testmodule' 109 | assert pkg.version == 'MODULE' 110 | 111 | assert utils.find_package('moult', installed, True) in pkg.dependencies 112 | assert utils.find_package('testpackage', installed, True) in pkg.dependencies 113 | 114 | 115 | def test_scan_shell_scripts(data): 116 | installed = data.copy_installed() 117 | data_dir = data.copy_data() 118 | shell_scripts = data_dir.join('scripts/shell') 119 | 120 | assert filesystem_scanner.scan(str(shell_scripts.join('bash_script')), installed) is None 121 | 122 | pkg = filesystem_scanner.scan(str(shell_scripts.join('python_script')), installed) 123 | assert pkg in installed 124 | assert pkg.version == 'SCRIPT' 125 | 126 | mpkg = utils.find_package('moult', installed, True) 127 | assert mpkg in pkg.dependencies 128 | 129 | data.verify_data() 130 | 131 | 132 | def test_bad_tabs(data): 133 | installed = data.copy_installed() 134 | data_dir = data.copy_data() 135 | bad_script = data_dir.join('scripts/loose/bad_tabs.py') 136 | 137 | mpkg1 = utils.find_package('moult', installed, True) 138 | mpkg2 = utils.find_package('testpackage', installed, True) 139 | 140 | pkg = filesystem_scanner.scan(str(bad_script), installed) 141 | assert pkg is not None 142 | assert mpkg1 in pkg.dependencies 143 | assert mpkg2 in pkg.dependencies 144 | 145 | data.verify_data() 146 | 147 | 148 | def test_unicode_scan(data): 149 | installed = data.copy_installed() 150 | data_dir = data.copy_data() 151 | unicode_script = data_dir.join('scripts/loose/unicode.py') 152 | 153 | mpkg = utils.find_package('moult', installed, True) 154 | pkg = filesystem_scanner.scan(unicode_script.strpath, installed) 155 | assert pkg is not None 156 | assert mpkg in pkg.dependencies 157 | 158 | 159 | def test_unicode_import_scan(data, capsys): 160 | installed = data.copy_installed() 161 | data_dir = data.copy_data() 162 | unicode_script = data_dir.join('scripts/loose/unicode_import.py') 163 | 164 | name = '\xe8\x84\xb1\xe7\x9a\xae' 165 | if sys.version_info[0] == 2: 166 | name = name.decode('utf8') 167 | japanesu = PyModule(name, '1.2.3', '/') 168 | installed.insert(0, japanesu) 169 | 170 | mpkg = utils.find_package('moult', installed, True) 171 | pkg = filesystem_scanner.scan(unicode_script.strpath, installed) 172 | assert pkg is not None 173 | assert mpkg in pkg.dependencies 174 | 175 | out, err = capsys.readouterr() 176 | 177 | if sys.version_info[0] == 2: 178 | assert japanesu not in pkg.dependencies 179 | assert japanesu.name in err 180 | elif sys.version_info[0] == 3: 181 | assert japanesu in installed 182 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | from moult import utils 6 | from moult.classes import PyModule 7 | 8 | 9 | python2_stdlib = ( 10 | '__builtin__', '__future__', 'abc', 'aifc', 'anydbm', 'argparse', 'array', 11 | 'ast', 'asynchat', 'asyncore', 'atexit', 'audioop', 'base64', 12 | 'BaseHTTPServer', 'bdb', 'binascii', 'binhex', 'bisect', 'bsddb', 'bz2', 13 | 'calendar', 'cgi', 'CGIHTTPServer', 'cgitb', 'chunk', 'cmath', 'cmd', 14 | 'code', 'codecs', 'codeop', 'collections', 'colorsys', 'compileall', 15 | 'ConfigParser', 'contextlib', 'Cookie', 'cookielib', 'copy', 'copy_reg', 16 | 'cPickle', 'cProfile', 'cStringIO', 'csv', 'ctypes', 'curses', 'datetime', 17 | 'dbhash', 'decimal', 'difflib', 'dis', 'distutils', 'doctest', 18 | 'DocXMLRPCServer', 'dumbdbm', 'dummy_thread', 'dummy_threading', 'email', 19 | 'encodings', 'errno', 'exceptions', 'filecmp', 'fileinput', 20 | 'fnmatch', 'formatter', 'fractions', 'ftplib', 'functools', 21 | 'future_builtins', 'gc', 'getopt', 'getpass', 'gettext', 'glob', 'gzip', 22 | 'hashlib', 'heapq', 'hmac', 'hotshot', 'htmlentitydefs', 'HTMLParser', 23 | 'httplib', 'imaplib', 'imghdr', 'imp', 'importlib', 'inspect', 'io', 24 | 'itertools', 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 25 | 'logging', 'macpath', 'mailbox', 'mailcap', 'marshal', 'math', 26 | 'mimetypes', 'mmap', 'modulefinder', 'multiprocessing', 'netrc', 27 | 'nntplib', 'numbers', 'operator', 'os', 'parser', 'pdb', 'pickle', 28 | 'pickletools', 'pkgutil', 'platform', 'plistlib', 'poplib', 'pprint', 29 | 'profile', 'pstats', 'py_compile', 'pyclbr', 'pydoc', 'Queue', 'quopri', 30 | 'random', 're', 'rlcompleter', 'robotparser', 'runpy', 'sched', 'select', 31 | 'shelve', 'shlex', 'shutil', 'signal', 'SimpleHTTPServer', 32 | 'SimpleXMLRPCServer', 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 33 | 'SocketServer', 'sqlite3', 'ssl', 'stat', 'string', 'StringIO', 34 | 'stringprep', 'struct', 'subprocess', 'sunau', 'symbol', 'symtable', 35 | 'sys', 'sysconfig', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 36 | 'test', 'textwrap', 'thread', 'threading', 'time', 'timeit', 'Tix', 37 | 'Tkinter', 'token', 'tokenize', 'trace', 'traceback', 'ttk', 'turtle', 38 | 'types', 'unicodedata', 'unittest', 'urllib', 'urllib2', 'urlparse', 39 | 'UserDict', 'UserList', 'UserString', 'uu', 'uuid', 'warnings', 'wave', 40 | 'weakref', 'webbrowser', 'whichdb', 'wsgiref', 'xdrlib', 'xml', 41 | 'xmlrpclib', 'zipfile', 'zipimport', 'zlib', 42 | ) 43 | 44 | 45 | python3_stdlib = ( 46 | '__future__', '_dummy_thread', '_thread', 'abc', 'aifc', 47 | 'argparse', 'array', 'ast', 'asynchat', 'asyncore', 'atexit', 'audioop', 48 | 'base64', 'bdb', 'binascii', 'binhex', 'bisect', 'builtins', 'bz2', 49 | 'calendar', 'cgi', 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs', 50 | 'codeop', 'collections', 'colorsys', 'compileall', 'concurrent', 51 | 'configparser', 'contextlib', 'copy', 'copyreg', 'cProfile', 'csv', 52 | 'ctypes', 'curses', 'datetime', 'dbm', 'decimal', 'difflib', 'dis', 53 | 'distutils', 'doctest', 'dummy_threading', 'email', 'encodings', 54 | 'errno', 'filecmp', 'fileinput', 'fnmatch', 'formatter', 'fractions', 55 | 'ftplib', 'functools', 'gc', 'getopt', 'getpass', 'gettext', 'glob', 56 | 'gzip', 'hashlib', 'heapq', 'hmac', 'html', 'http', 'imaplib', 'imghdr', 57 | 'imp', 'importlib', 'inspect', 'io', 'itertools', 'json', 'keyword', 58 | 'lib2to3', 'linecache', 'locale', 'logging', 'macpath', 'mailbox', 59 | 'mailcap', 'marshal', 'math', 'mimetypes', 'mmap', 'modulefinder', 60 | 'multiprocessing', 'netrc', 'nntplib', 'numbers', 'operator', 'os', 61 | 'parser', 'pdb', 'pickle', 'pickletools', 'pkgutil', 'platform', 62 | 'plistlib', 'poplib', 'pprint', 'profile', 'pstats', 'py_compile', 63 | 'pyclbr', 'pydoc', 'queue', 'quopri', 'random', 're', 'reprlib', 64 | 'rlcompleter', 'runpy', 'sched', 'select', 'shelve', 'shlex', 'shutil', 65 | 'signal', 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 66 | 'socketserver', 'sqlite3', 'ssl', 'stat', 'string', 'struct', 67 | 'subprocess', 'sunau', 'symbol', 'symtable', 'sys', 'sysconfig', 68 | 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'test', 'textwrap', 69 | 'threading', 'time', 'timeit', 'tkinter', 'token', 'tokenize', 'trace', 70 | 'traceback', 'turtle', 'types', 'unicodedata', 'unittest', 'urllib', 71 | 'uu', 'uuid', 'warnings', 'wave', 'weakref', 'webbrowser', 'wsgiref', 72 | 'xdrlib', 'xml', 'xmlrpc', 'zipfile', 'zipimport', 'zlib', 73 | ) 74 | 75 | 76 | def test_installed_packages(data): 77 | installed = data.copy_installed() 78 | assert len(installed) 79 | 80 | for pkg in installed: 81 | assert isinstance(pkg, PyModule), 'Item in installed packages is not a PyModule instance' 82 | assert len(pkg.import_names), 'Installed package has no import path names' 83 | pkg2 = PyModule(pkg.name, pkg.version, pkg.location, pkg.missing) 84 | assert hash(pkg) == hash(pkg2), '__hash__ failure in PyModule' 85 | 86 | 87 | @pytest.mark.skipif(sys.version_info[:2] > (2, 7), reason='Requires Python 2.7') 88 | def test_python2_stdlib(): 89 | print(sys.version_info) 90 | for lib in python2_stdlib: 91 | assert utils.is_stdlib(lib) 92 | 93 | 94 | @pytest.mark.skipif(sys.version_info[:2] < (3, 2), reason='Requires Python 3.2+') 95 | def test_python3_stdlib(): 96 | print(sys.version_info) 97 | for lib in python3_stdlib: 98 | assert utils.is_stdlib(lib) 99 | 100 | 101 | def test_not_stdlib(): 102 | assert not utils.is_stdlib('moult') 103 | assert not utils.is_stdlib('moult.utils') 104 | assert not utils.is_stdlib('spam') 105 | assert not utils.is_stdlib('spam.eggs.bacon') 106 | assert not utils.is_stdlib('somefile.py') 107 | assert not utils.is_stdlib('/some/path') 108 | 109 | 110 | def test_is_import_str(): 111 | assert utils.is_import_str('spam.eggs.cheese') 112 | assert not utils.is_import_str('spam') 113 | assert not utils.is_import_str('/path/to/someplace.txt') 114 | 115 | 116 | def test_resolve_import(): 117 | from_module = 'spam.eggs.cheese' 118 | 119 | assert utils.resolve_import('.bacon', from_module) == 'spam.eggs.bacon' 120 | assert utils.resolve_import('..bacon', from_module) == 'spam.bacon' 121 | assert utils.resolve_import('...bacon', from_module) == 'bacon' 122 | assert utils.resolve_import('....bacon', from_module) == 'bacon' 123 | 124 | 125 | def test_find_package(data): 126 | installed = data.copy_installed() 127 | pkg = utils.find_package('moult', installed, package=True) 128 | assert pkg and pkg.name == 'moult' 129 | 130 | pkg = utils.find_package('moult.utils', installed, package=True) 131 | assert pkg is None 132 | 133 | pkg = utils.find_package('does.not.exist', installed) 134 | assert pkg is None 135 | 136 | pkg = utils.find_package('moult', installed) 137 | assert pkg and pkg.name == 'moult' 138 | 139 | # Searching for module imports should only match their top level 140 | # import names 141 | pkg = utils.find_package('moult.utils.does.not.exist', installed) 142 | assert pkg is None 143 | 144 | pkg = utils.find_package('pkg_resources', installed) 145 | assert pkg and pkg.name == 'setuptools' 146 | 147 | 148 | def test_find_file(data): 149 | data_dir = data.copy_data() 150 | filename = str(data_dir.join('scripts/project/nested/scripts/testmodule/utils/spam.py')) 151 | root = str(data_dir.join('scripts/project/nested/scripts/testmodule')) 152 | import_file = utils.file_containing_import('utils.spam.SomeClass', root) 153 | assert import_file == filename 154 | 155 | 156 | def test_file_types(data): 157 | datadir = data.copy_data() 158 | bash_script = str(datadir.join('scripts/shell/bash_script')) 159 | py_script = str(datadir.join('scripts/shell/python_script')) 160 | 161 | assert utils.is_script(bash_script) 162 | assert not utils.is_python_script(bash_script) 163 | assert utils.is_script(py_script) 164 | assert utils.is_python_script(py_script) 165 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = pep8, py{27,33,34}, pypy, pypy3 3 | 4 | [testenv:pep8] 5 | basepython = python2.7 6 | deps = pytest-flake8 7 | commands = py.test --flake8 moult/ 8 | 9 | [testenv] 10 | deps = 11 | pytest 12 | testpackage 13 | django 14 | django-allauth 15 | django-boto 16 | django-mptt 17 | commands = py.test 18 | 19 | [pytest] 20 | flake8-ignore = E126 E127 E501 F403 F405 21 | --------------------------------------------------------------------------------