├── .bumpversion.cfg ├── .coveragerc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .python-version ├── .travis.yml ├── LICENSE ├── README.rst ├── dev-requirements.txt ├── docs ├── Makefile ├── api │ └── index.rst ├── conf.py ├── index.rst ├── make.bat └── user_guide │ └── index.rst ├── pike ├── __init__.py ├── discovery │ ├── __init__.py │ ├── filesystem.py │ └── py.py ├── finder.py ├── loader.py └── manager.py ├── poetry.lock ├── pyproject.toml ├── requirements.txt ├── test-requirements.txt ├── tests ├── __init__.py ├── conftest.py ├── discovery │ ├── __init__.py │ ├── test_filesystem.py │ └── test_py.py ├── test_finder.py ├── test_loader.py ├── test_manager.py └── utils.py └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:pyproject.toml] 7 | 8 | [bumpversion:file:docs/conf.py] 9 | 10 | [bumpversion:file:pike/__init__.py] 11 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = pike 4 | omit = tests/* 5 | 6 | [report] 7 | exclude_lines = 8 | if six.PY3: 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: [3.8, 3.9, 3.10.17, 3.11, 3.12] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | pip install tox codecov 25 | - name: Run tests with tox 26 | run: tox -e py 27 | - name: Upload coverage to Codecov 28 | uses: codecov/codecov-action@v4 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Editor 7 | *.kate-swp 8 | *.~ 9 | *~ 10 | *.swp 11 | .idea 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | env/ 19 | .env2/ 20 | .env3/ 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *,cover 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.7.16 2 | 3.8.9 3 | 3.9.9 4 | 3.10.9 5 | 3.11.4 6 | 3.12.0 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | cache: 4 | directories: 5 | - "$HOME/.cache/pip" 6 | matrix: 7 | include: 8 | - python: 3.8 9 | env: TOXENV=flake8 10 | - python: 3.7 11 | env: TOXENV=py37 12 | - python: 3.8 13 | env: TOXENV=py38 14 | - python: 3.9 15 | env: TOXENV=py39 16 | - python: 3.10 17 | env: TOXENV=py310 18 | - python: 3.11 19 | env: TOXENV=py311 20 | - python: 3.12 21 | env: TOXENV=py312 22 | install: 23 | - pip install tox codecov twine 24 | script: 25 | - tox -e $TOXENV 26 | after_success: 27 | - codecov 28 | deploy: 29 | provider: pypi 30 | user: pyarmory 31 | password: 32 | secure: kCrsMsT2c8/UcbV4hukx/bOoXaHLlgY1EVYLVyzu9WdOmVDjFICF845VfFIjigbks8JrMPO1jtRJSjui+65yRWUxbPh5BmMDQBcbU18q8y/xUoVGW9fHgalZuWqUms8YOTgGHgUNHhzbRwNEBc1CAaertUiwT6D+u3LLW05O1jZGNMcWeUeIYL7WCzeNVGLWYorgPlwcKOwGef3uZuA9niDHQSHBA/S4Dft/jbXhUBloGIz61Rc0xpASJJRmM2Y2K6uLlJqmvQ/6iQzEUgbAwcK7fkM8QueMXBPRHQhgB/qKB6zgIUu/RjPaJSBQlfbsVxdcva06F1yoaMeE3v5L4Sqb1hrM/YP2+FK8g0I0apD8FFfa6NtqzX3dnQtSgWQsFi89G8uh1c7bCqZpJyI8x/DW0cMnrlzgqbH61C88xRrMWHoD1cEY+VzoyhtqvFdQm1Z+K/YwYbHSvxlX88PoaAfHgYorUzN5aBmaxAQophoUoobWuMxH3N48G7z+4wYlojGM52AkFsU0Ku+2YspdLKhBxm9HEtGujsdPMjARuB8bQZQF55aBfVzmB4vsP8VwkHvb1iVUv5Q1uA+FxVnX8VaP+g8pb9aCLoSxa5BkXm+KWdgTKZPO3cEZ2OPRv0oP5fz3pDkVXY5KhstUTOYObfgs+YO7RMTX+NPdSeUyjmU= 33 | distributions: "sdist bdist_wheel" 34 | on: 35 | tags: true 36 | repo: pyarmory/pike 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 John Vrbanac 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Pike 2 | ==== 3 | 4 | .. image:: https://travis-ci.org/pyarmory/pike.svg?branch=master 5 | :target: https://travis-ci.org/pyarmory/pike 6 | :alt: Travis CI Build 7 | 8 | .. image:: http://codecov.io/github/pyarmory/pike/coverage.svg?branch=master 9 | :target: http://codecov.io/github/pyarmory/pike?branch=master 10 | :alt: Coverage 11 | 12 | .. image:: https://readthedocs.org/projects/pyarmory-pike/badge/?version=latest 13 | :target: https://readthedocs.org/projects/pyarmory-pike/?badge=latest 14 | 15 | .. image:: https://codeclimate.com/github/pyarmory/pike/badges/gpa.svg 16 | :target: https://codeclimate.com/github/pyarmory/pike 17 | :alt: Code Climate 18 | 19 | 20 | Pike is a dynamic plugin management library for Python. Unlike most Python 21 | plugin managers, Pike allows for you to load Python packages from anywhere 22 | on a filesystem without complicated configuration. This enables applications 23 | to easily add the ability to expand their functionality through plugin modules. 24 | In addition to plugin management, Pike also includes a set of discovery 25 | functions for custom module and class discovery. 26 | 27 | * Documentation: ReadTheDocs_ 28 | * CI: Travis_ 29 | * Coverage: CodeCov_ 30 | 31 | 32 | .. _ReadTheDocs: http://pyarmory-pike.readthedocs.org/ 33 | .. _Travis: https://travis-ci.org/pyarmory/pike 34 | .. _CodeCov: https://codecov.io/github/pyarmory/pike?branch=master 35 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | -r test-requirements.txt 3 | tox 4 | twine 5 | sphinx 6 | sphinx_rtd_theme 7 | sphinxcontrib-spelling 8 | bumpversion 9 | -------------------------------------------------------------------------------- /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/Pike.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Pike.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/Pike" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Pike" 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/api/index.rst: -------------------------------------------------------------------------------- 1 | Classes and Functions 2 | ===================== 3 | 4 | Plugin Manager 5 | -------------- 6 | 7 | .. autoclass:: pike.manager.PikeManager 8 | :members: 9 | 10 | Discovery Classes 11 | ----------------- 12 | 13 | Python 14 | ^^^^^^ 15 | 16 | .. automodule:: pike.discovery.py 17 | :members: 18 | 19 | Filesystem 20 | ^^^^^^^^^^ 21 | 22 | .. automodule:: pike.discovery.filesystem 23 | :members: 24 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Pike documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Jul 8 22:30:15 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 sphinx_rtd_theme 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 | extensions = [ 27 | 'sphinx.ext.autodoc', 28 | 'sphinx.ext.doctest', 29 | 'sphinx.ext.coverage', 30 | ] 31 | 32 | # Add any paths that contain templates here, relative to this directory. 33 | templates_path = ['_templates'] 34 | 35 | # The suffix(es) of source filenames. 36 | # You can specify multiple suffix as a list of string: 37 | # source_suffix = ['.rst', '.md'] 38 | source_suffix = '.rst' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'Pike' 45 | copyright = u'2015, John Vrbanac' 46 | author = u'John Vrbanac' 47 | 48 | # The version info for the project you're documenting, acts as replacement for 49 | # |version| and |release|, also used in various other places throughout the 50 | # built documents. 51 | # 52 | # The short X.Y version. 53 | version = '0.2.0' 54 | # The full version, including alpha/beta/rc tags. 55 | release = version 56 | 57 | # The language for content autogenerated by Sphinx. Refer to documentation 58 | # for a list of supported languages. 59 | # 60 | # This is also used if you do content translation via gettext catalogs. 61 | # Usually you set "language" from the command line for these cases. 62 | language = None 63 | 64 | # List of patterns, relative to source directory, that match files and 65 | # directories to ignore when looking for source files. 66 | exclude_patterns = ['_build'] 67 | 68 | # If true, the current module name will be prepended to all description 69 | # unit titles (such as .. function::). 70 | # add_module_names = True 71 | 72 | # If true, sectionauthor and moduleauthor directives will be shown in the 73 | # output. They are ignored by default. 74 | # show_authors = False 75 | 76 | # The name of the Pygments (syntax highlighting) style to use. 77 | pygments_style = 'sphinx' 78 | 79 | # A list of ignored prefixes for module index sorting. 80 | # modindex_common_prefix = [] 81 | 82 | # If true, `todo` and `todoList` produce output, else they produce nothing. 83 | todo_include_todos = False 84 | 85 | # -- Options for Autodoc 86 | 87 | autoclass_content = 'init' 88 | 89 | # -- Options for HTML output ---------------------------------------------- 90 | 91 | # The theme to use for HTML and HTML Help pages. See the documentation for 92 | # a list of builtin themes. 93 | html_theme = "sphinx_rtd_theme" 94 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 95 | 96 | # Add any paths that contain custom static files (such as style sheets) here, 97 | # relative to this directory. They are copied after the builtin static files, 98 | # so a file named "default.css" will overwrite the builtin "default.css". 99 | html_static_path = ['_static'] 100 | 101 | # If true, links to the reST sources are added to the pages. 102 | # html_show_sourcelink = True 103 | 104 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 105 | # html_show_sphinx = True 106 | 107 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 108 | # html_show_copyright = True 109 | 110 | # If true, an OpenSearch description file will be output, and all pages will 111 | # contain a tag referring to it. The value of this option must be the 112 | # base URL from which the finished HTML is served. 113 | # html_use_opensearch = '' 114 | 115 | # Output file base name for HTML help builder. 116 | htmlhelp_basename = 'Pikedoc' 117 | 118 | # -- Options for LaTeX output --------------------------------------------- 119 | 120 | latex_elements = { 121 | } 122 | 123 | # Grouping the document tree into LaTeX files. List of tuples 124 | # (source start file, target name, title, 125 | # author, documentclass [howto, manual, or own class]). 126 | latex_documents = [ 127 | (master_doc, 'Pike.tex', u'Pike Documentation', 128 | u'John Vrbanac', 'manual'), 129 | ] 130 | 131 | # -- Options for manual page output --------------------------------------- 132 | 133 | # One entry per manual page. List of tuples 134 | # (source start file, name, description, authors, manual section). 135 | man_pages = [ 136 | (master_doc, 'pike', u'Pike Documentation', 137 | [author], 1) 138 | ] 139 | 140 | 141 | # -- Options for Texinfo output ------------------------------------------- 142 | 143 | # Grouping the document tree into Texinfo files. List of tuples 144 | # (source start file, target name, title, author, 145 | # dir menu entry, description, category) 146 | texinfo_documents = [ 147 | (master_doc, 'Pike', u'Pike Documentation', 148 | author, 'Pike', 'One line description of project.', 149 | 'Miscellaneous'), 150 | ] 151 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Pike's documentation! 2 | ================================ 3 | 4 | Pike is a dynamic plugin management library for Python. Unlike most Python 5 | plugin managers, Pike allows for you to load Python packages from anywhere 6 | on a filesystem without complicated configuration. This enables applications 7 | to easily add the ability to expand their functionality through plugin modules. 8 | 9 | Documentation 10 | ------------- 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | 15 | user_guide/index 16 | api/index 17 | 18 | 19 | 20 | Indices and tables 21 | ================== 22 | 23 | * :ref:`genindex` 24 | * :ref:`modindex` 25 | * :ref:`search` 26 | 27 | -------------------------------------------------------------------------------- /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\Pike.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Pike.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 | -------------------------------------------------------------------------------- /docs/user_guide/index.rst: -------------------------------------------------------------------------------- 1 | User Guide 2 | ========== 3 | 4 | Get Started 5 | ----------- 6 | 7 | The common use-case for Pike is to enable dynamic loading of Python packages 8 | from various locations on a user's filesystem. This is usually to facilitate 9 | the usage of plugins. 10 | 11 | The easiest way to use Pike to load Python packages is to use it as a context 12 | manager: 13 | 14 | .. code-block:: python 15 | 16 | from pike.manager import PikeManager 17 | 18 | with PikeManager(['/path/containing/python/packages']) as mgr: 19 | classes = mgr.get_classes() 20 | 21 | 22 | If you need to use Pike for an extended period of time (such as for testing), 23 | you can use a normal instance of Pike. However, the downside to that is that 24 | you'll need to manually trigger Pike to cleanup itself when you're done. 25 | 26 | .. code-block:: python 27 | 28 | from pike.manager import PikeManager 29 | 30 | manager = PikeManager(['/path/containing/python/packages']) 31 | classes = manager.get_classes() 32 | manager.cleanup() 33 | 34 | 35 | Discovery 36 | ---------- 37 | 38 | Pike also includes a set of discovery functions to allow for someone to find 39 | modules or classes that have been imported or that are available on a filesystem. 40 | 41 | * Documentation for imported module discovery: :mod:`pike.discovery.py` 42 | * Documentation for filesystem discovery: :mod:`pike.discovery.filesystem` 43 | 44 | 45 | Installation 46 | ------------ 47 | 48 | Install from PyPI 49 | ^^^^^^^^^^^^^^^^^ 50 | 51 | .. code-block:: shell 52 | 53 | pip install --upgrade pike 54 | 55 | Install from source 56 | ^^^^^^^^^^^^^^^^^^^ 57 | 58 | You can find the source for Pike located on GitHub_. Once downloaded you can 59 | install Pike using pip. 60 | 61 | If you want to just do a normal source install of Pike the execute: 62 | 63 | .. code-block:: shell 64 | 65 | # In the Pike source directory 66 | pip install . 67 | 68 | If you want to make changes to Pike, then install execute: 69 | 70 | .. code-block:: shell 71 | 72 | # In the Pike source directory 73 | pip install -e . 74 | 75 | 76 | .. _GitHub: https://github.com/pyarmory/pike 77 | -------------------------------------------------------------------------------- /pike/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.2.0" 2 | -------------------------------------------------------------------------------- /pike/discovery/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyarmory/pike/d2094b565044d9d2f6998b831aeecc5546d645e7/pike/discovery/__init__.py -------------------------------------------------------------------------------- /pike/discovery/filesystem.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | EXCLUDED_MODULE_NAMES = ['__init__.py'] 4 | 5 | 6 | def is_package(path): 7 | """Checks if path string is a package""" 8 | return os.path.exists(os.path.join(path, '__init__.py')) 9 | 10 | 11 | def is_module(path): 12 | """Checks if path string is a module""" 13 | return path.endswith('.py') 14 | 15 | 16 | def get_name(path): 17 | filename = os.path.basename(path) 18 | name, _ = os.path.splitext(filename) 19 | return name 20 | 21 | 22 | def find_modules(path): 23 | """Finds all modules located on a path""" 24 | for pathname in os.listdir(path): 25 | if pathname in EXCLUDED_MODULE_NAMES: 26 | continue 27 | 28 | full_path = os.path.join(path, pathname) 29 | if os.path.isfile(full_path) and is_module(full_path): 30 | yield full_path 31 | 32 | 33 | def find_packages(path): 34 | """Finds all packages located on a path""" 35 | for pathname in os.listdir(path): 36 | full_path = os.path.join(path, pathname) 37 | if os.path.isdir(full_path) and is_package(full_path): 38 | yield full_path 39 | 40 | 41 | def recursive_find_packages(path): 42 | """Recursively finds all packages located on a path""" 43 | for pkg in find_packages(path): 44 | yield pkg 45 | for sub_pkg in recursive_find_packages(pkg): 46 | yield sub_pkg 47 | 48 | 49 | def recursive_find_modules(path): 50 | """Recursively finds all modules located on a path""" 51 | for module_path in find_modules(path): 52 | yield module_path 53 | 54 | for pkg_path in recursive_find_packages(path): 55 | for module_path in find_modules(pkg_path): 56 | yield module_path 57 | -------------------------------------------------------------------------------- /pike/discovery/py.py: -------------------------------------------------------------------------------- 1 | import os 2 | import importlib 3 | import inspect 4 | 5 | from pike.discovery import filesystem 6 | 7 | 8 | def get_module_by_name(full_module_name): 9 | """Import module by full name 10 | 11 | :param str full_module_name: Full module name e.g. (pike.discovery.py) 12 | :return: Imported :class:`module` 13 | """ 14 | return importlib.import_module(full_module_name) 15 | 16 | 17 | def is_child_of_module(obj, parent): 18 | package, _, name = obj.__name__.rpartition('.') 19 | return package.startswith(parent.__name__) 20 | 21 | 22 | def _import_from_path(path, package_name): 23 | module_name = filesystem.get_name(path) 24 | fullname = '{}.{}'.format(package_name, module_name) 25 | 26 | return get_module_by_name(fullname) 27 | 28 | 29 | def _child_modules(module): 30 | package_path = os.path.dirname(inspect.getabsfile(module)) 31 | 32 | # Import all child modules 33 | for module_path in filesystem.find_modules(package_path): 34 | _import_from_path(module_path, module.__package__ or module.__name__) 35 | 36 | # Import all sub packages 37 | for package_path in filesystem.find_packages(package_path): 38 | _import_from_path(package_path, module.__package__ or module.__name__) 39 | 40 | for name, obj in inspect.getmembers(module): 41 | if inspect.ismodule(obj) and is_child_of_module(obj, module): 42 | yield obj 43 | 44 | 45 | def classes_in_module(module, filter_func=None): 46 | """Retrieve classes within a module 47 | 48 | :param module module: Module to search under 49 | :param Function filter_func: Custom filter function(cls_obj). 50 | :return: :class:`generator` containing classes within a module 51 | """ 52 | finder_filter = filter_func or (lambda x: True) 53 | 54 | for name, obj in inspect.getmembers(module): 55 | if inspect.isclass(obj) and finder_filter(obj): 56 | yield obj 57 | 58 | 59 | def get_inherited_classes(module, base_class): 60 | """Retrieve inherited classes from a single module 61 | 62 | :param module module: Module to search under 63 | :param Class base_class: Base class to filter results by 64 | :return: :class:`List` of all found classes 65 | """ 66 | def class_filter(cls): 67 | return cls != base_class and issubclass(cls, base_class) 68 | 69 | return list(classes_in_module(module, class_filter)) 70 | 71 | 72 | def get_child_modules(module, recursive=True): 73 | """Retrieve child modules 74 | 75 | :param module module: Module to search under 76 | :param bool recursive: Toggles the retrieval of sub-children module. 77 | :return: :class:`generator` containing child modules 78 | """ 79 | for child_module in _child_modules(module): 80 | yield child_module 81 | 82 | if recursive: 83 | for sub_child_module in _child_modules(child_module): 84 | yield sub_child_module 85 | 86 | 87 | def get_all_classes(module, filter_func=None): 88 | """Retrieve all classes from modules 89 | 90 | :param module module: Module to search under 91 | :param Function filter_func: Custom filter function(cls_obj). 92 | :returns: :class:`List` of all found classes 93 | """ 94 | all_classes = [] 95 | 96 | # Current module's classes 97 | all_classes.extend([cls for cls in classes_in_module(module, filter_func)]) 98 | 99 | # All child module classes 100 | for child_module in get_child_modules(module): 101 | child_module_classes = classes_in_module(child_module, filter_func) 102 | all_classes.extend([cls for cls in child_module_classes]) 103 | 104 | # TODO(jmvrbanac): Rework this so that we don't have to use a set 105 | return list(set(all_classes)) 106 | 107 | 108 | def get_all_inherited_classes(module, base_class): 109 | """Retrieve all inherited classes from modules 110 | 111 | :param module module: Module to search under 112 | :param Class base_class: Base class to filter results by 113 | :return: :class:`List` of all found classes 114 | """ 115 | def class_filter(cls): 116 | return cls != base_class and issubclass(cls, base_class) 117 | 118 | return get_all_classes(module, class_filter) 119 | -------------------------------------------------------------------------------- /pike/finder.py: -------------------------------------------------------------------------------- 1 | import os 2 | import importlib.util 3 | import importlib.abc 4 | 5 | from pike.loader import PikeLoader 6 | 7 | 8 | class PikeFinder(importlib.abc.MetaPathFinder): 9 | def __init__(self, paths=None): 10 | self.paths = paths or [] 11 | 12 | def module_name_to_filename(self, fullname): 13 | separated_name = fullname.split('.') 14 | return os.path.join(*separated_name) 15 | 16 | def get_import_filename(self, module_path): 17 | for base_path in self.paths: 18 | target_path = os.path.join(base_path, module_path) 19 | is_pkg = os.path.isdir(target_path) 20 | 21 | if is_pkg: 22 | filename = os.path.join(target_path, '__init__.py') 23 | else: 24 | filename = '{}.py'.format(target_path) 25 | 26 | if os.path.exists(filename): 27 | return filename 28 | 29 | def find_module(self, fullname, path=None): 30 | converted_name = self.module_name_to_filename(fullname) 31 | module_path = self.get_import_filename(converted_name) 32 | 33 | if module_path: 34 | return PikeLoader(fullname, module_path) 35 | 36 | def find_spec(self, fullname, path, target=None): 37 | mod_name = fullname.rsplit('.', 1)[-1] 38 | 39 | # We want to avoid collisions with third-party packages 40 | if path: 41 | for search_path in self.paths: 42 | if not path[0].startswith(search_path): 43 | return None 44 | 45 | # Only handle modules/packages that are directly in one of our search paths 46 | for base_path in self.paths: 47 | package_dir = os.path.join(base_path, mod_name) 48 | init_file = os.path.join(package_dir, '__init__.py') 49 | if os.path.isdir(package_dir) and os.path.isfile(init_file): 50 | loader = PikeLoader(fullname, init_file) 51 | return importlib.util.spec_from_file_location( 52 | fullname, 53 | init_file, 54 | loader=loader, 55 | submodule_search_locations=[package_dir] 56 | ) 57 | 58 | # Check for a plain module: /.py 59 | module_file = os.path.join(base_path, mod_name + '.py') 60 | if os.path.isfile(module_file): 61 | loader = PikeLoader(fullname, module_file) 62 | return importlib.util.spec_from_file_location( 63 | fullname, 64 | module_file, 65 | loader=loader 66 | ) 67 | # Not in our search paths: let standard loaders handle it 68 | return None 69 | -------------------------------------------------------------------------------- /pike/loader.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import importlib.util 4 | import importlib.abc 5 | 6 | 7 | class PikeLoader(importlib.abc.Loader): 8 | def __init__(self, fullname, module_path): 9 | self.fullname = fullname 10 | self.module_path = module_path 11 | 12 | def is_package(self, fullname=None): 13 | """ 14 | :param fullname: Not used, but required for Python 3.4 15 | """ 16 | 17 | filename = os.path.basename(self.module_path) 18 | return filename.startswith('__init__') 19 | 20 | def augment_module(self, fullname, module): 21 | package, _, _ = fullname.rpartition('.') 22 | 23 | if self.is_package(): 24 | module.__path__ = [self.module_path] 25 | module.__package__ = fullname 26 | else: 27 | module.__package__ = package 28 | 29 | return module 30 | 31 | def create_module(self, spec): 32 | # Use default module creation semantics 33 | return None 34 | 35 | def exec_module(self, module): 36 | # Actually execute the code in the module 37 | with open(self.module_path, 'rb') as f: 38 | code = compile(f.read(), self.module_path, 'exec') 39 | exec(code, module.__dict__) 40 | 41 | def load_module(self, fullname): 42 | if self.fullname != fullname: 43 | raise ImportError('Cannot import module with this loader') 44 | 45 | if fullname in sys.modules: 46 | return sys.modules[fullname] 47 | 48 | module = self.load_module_by_path(fullname, self.module_path) 49 | sys.modules[fullname] = module 50 | return module 51 | 52 | def load_module_by_path(self, module_name, path): 53 | _, ext = os.path.splitext(path) 54 | module = None 55 | 56 | # FIXME(jmvrbanac): Get this working properly in PY3 57 | # Python 3 - Try to get the cache filename 58 | # if six.PY3: 59 | # compiled_filename = imp.cache_from_source(path) 60 | # if os.path.exists(compiled_filename): 61 | # path, ext = compiled_filename, '.pyc' 62 | 63 | # if ext.lower() == '.pyc': 64 | # module = imp.load_compiled(module_name, path) 65 | # elif ext.lower() == '.py': 66 | if ext.lower() == '.py': 67 | spec = importlib.util.spec_from_file_location(module_name, path) 68 | if spec is None or spec.loader is None: 69 | raise ImportError(f"Cannot load module {module_name} from {path}") 70 | module = importlib.util.module_from_spec(spec) 71 | sys.modules[module_name] = module # <-- This is key! 72 | spec.loader.exec_module(module) 73 | 74 | if module: 75 | # Make sure we properly fill-in __path__ and __package__ 76 | module = self.augment_module(module_name, module) 77 | 78 | return module 79 | -------------------------------------------------------------------------------- /pike/manager.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from pike.discovery import filesystem 4 | from pike.discovery import py 5 | from pike.finder import PikeFinder 6 | 7 | 8 | class PikeManager(object): 9 | def __init__(self, search_paths=None): 10 | """The Pike plugin manager 11 | 12 | The manager allows for the dynamic loading of Python packages for any 13 | location on a user's filesystem. 14 | 15 | :param list search_paths: List of path strings to include during module 16 | importing. These paths are only in addition to existing Python 17 | import search paths. 18 | 19 | 20 | Using PikeManager as a context manager: 21 | 22 | .. code-block:: python 23 | 24 | from pike.manager import PikeManager 25 | 26 | with PikeManager(['/path/containing/package']) as mgr: 27 | import module_in_the_package 28 | 29 | Using PikeManager instance: 30 | 31 | .. code-block:: python 32 | 33 | from pike.manager import PikeManager 34 | 35 | mgr = PikeManager(['/path/container/package']) 36 | import module_in_the_package 37 | mgr.cleanup() 38 | """ 39 | self.search_paths = search_paths or [] 40 | self.module_finder = PikeFinder(search_paths) 41 | self.add_to_meta_path() 42 | 43 | def cleanup(self): 44 | """Removes Pike's import hooks 45 | 46 | This should be called if an implementer is not using the manager as 47 | a context manager. 48 | """ 49 | if sys.meta_path and self.module_finder in sys.meta_path: 50 | sys.meta_path.remove(self.module_finder) 51 | 52 | def add_to_meta_path(self): 53 | """Adds Pike's import hooks to Python 54 | 55 | This should be automatically handled by Pike; however, this is method 56 | is accessible for very rare use-cases. 57 | """ 58 | if self.module_finder in sys.meta_path: 59 | return 60 | 61 | sys.meta_path.insert(0, self.module_finder) 62 | 63 | def get_classes(self, filter_func=None): 64 | """Get all classes within modules on the manager's search paths 65 | 66 | :param Function filter_func: Custom filter function(cls_obj). 67 | :returns: :class:`List` of all found classes 68 | """ 69 | all_classes = [] 70 | # Top-most Modules 71 | for module_name in self.get_module_names(): 72 | module = py.get_module_by_name(module_name) 73 | all_classes.extend(py.classes_in_module(module, filter_func)) 74 | 75 | # All packages 76 | for module_name in self.get_package_names(): 77 | module = py.get_module_by_name(module_name) 78 | all_classes.extend(py.get_all_classes(module, filter_func)) 79 | 80 | return all_classes 81 | 82 | def get_all_inherited_classes(self, base_class): 83 | """Retrieve all inherited classes from manager's search paths 84 | 85 | :param Class base_class: Base class to filter results by 86 | :return: :class:`List` of all found classes 87 | """ 88 | all_classes = [] 89 | # Top-most Modules 90 | for module_name in self.get_module_names(): 91 | module = py.get_module_by_name(module_name) 92 | all_classes.extend(py.get_inherited_classes(module, base_class)) 93 | 94 | # All packages 95 | for module_name in self.get_package_names(): 96 | module = py.get_module_by_name(module_name) 97 | inherited = py.get_all_inherited_classes(module, base_class) 98 | all_classes.extend(inherited) 99 | 100 | return all_classes 101 | 102 | def get_module_names(self): 103 | """Get root module names available on the manager's search paths 104 | 105 | :returns: :class:`generator` providing available module names. 106 | """ 107 | for path in self.search_paths: 108 | for package_path in filesystem.find_modules(path): 109 | yield filesystem.get_name(package_path) 110 | 111 | def get_package_names(self): 112 | """Get root package names available on the manager's search paths 113 | 114 | :returns: :class:`generator` providing available package names. 115 | """ 116 | for path in self.search_paths: 117 | for package_path in filesystem.find_packages(path): 118 | yield filesystem.get_name(package_path) 119 | 120 | def __enter__(self): 121 | return self 122 | 123 | def __exit__(self, type, value, traceback): 124 | self.cleanup() 125 | 126 | def __del__(self): 127 | self.cleanup() 128 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "colorama" 5 | version = "0.4.6" 6 | description = "Cross-platform colored terminal text." 7 | optional = false 8 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 9 | files = [ 10 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 11 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 12 | ] 13 | 14 | [[package]] 15 | name = "coverage" 16 | version = "7.2.7" 17 | description = "Code coverage measurement for Python" 18 | optional = false 19 | python-versions = ">=3.7" 20 | files = [ 21 | {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, 22 | {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, 23 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, 24 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, 25 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, 26 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, 27 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, 28 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, 29 | {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, 30 | {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, 31 | {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, 32 | {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, 33 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, 34 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, 35 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, 36 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, 37 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, 38 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, 39 | {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, 40 | {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, 41 | {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, 42 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, 43 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, 44 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, 45 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, 46 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, 47 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, 48 | {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, 49 | {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, 50 | {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, 51 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, 52 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, 53 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, 54 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, 55 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, 56 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, 57 | {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, 58 | {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, 59 | {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, 60 | {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, 61 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, 62 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, 63 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, 64 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, 65 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, 66 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, 67 | {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, 68 | {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, 69 | {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, 70 | {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, 71 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, 72 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, 73 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, 74 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, 75 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, 76 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, 77 | {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, 78 | {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, 79 | {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, 80 | {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, 81 | ] 82 | 83 | [package.extras] 84 | toml = ["tomli"] 85 | 86 | [[package]] 87 | name = "exceptiongroup" 88 | version = "1.3.0" 89 | description = "Backport of PEP 654 (exception groups)" 90 | optional = false 91 | python-versions = ">=3.7" 92 | files = [ 93 | {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, 94 | {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, 95 | ] 96 | 97 | [package.dependencies] 98 | typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} 99 | 100 | [package.extras] 101 | test = ["pytest (>=6)"] 102 | 103 | [[package]] 104 | name = "flake8" 105 | version = "5.0.3" 106 | description = "the modular source code checker: pep8 pyflakes and co" 107 | optional = false 108 | python-versions = ">=3.6.1" 109 | files = [ 110 | {file = "flake8-5.0.3-py2.py3-none-any.whl", hash = "sha256:93aa565ae2f0316b95bb57a354f2b2d55ee8508e1fe1cb13b77b9c195b4a2537"}, 111 | {file = "flake8-5.0.3.tar.gz", hash = "sha256:b27fd7faa8d90aaae763664a489012292990388e5d3604f383b290caefbbc922"}, 112 | ] 113 | 114 | [package.dependencies] 115 | importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} 116 | mccabe = ">=0.7.0,<0.8.0" 117 | pycodestyle = ">=2.9.0,<2.10.0" 118 | pyflakes = ">=2.5.0,<2.6.0" 119 | 120 | [[package]] 121 | name = "importlib-metadata" 122 | version = "4.2.0" 123 | description = "Read metadata from Python packages" 124 | optional = false 125 | python-versions = ">=3.6" 126 | files = [ 127 | {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, 128 | {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, 129 | ] 130 | 131 | [package.dependencies] 132 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 133 | zipp = ">=0.5" 134 | 135 | [package.extras] 136 | docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] 137 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] 138 | 139 | [[package]] 140 | name = "iniconfig" 141 | version = "2.0.0" 142 | description = "brain-dead simple config-ini parsing" 143 | optional = false 144 | python-versions = ">=3.7" 145 | files = [ 146 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 147 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 148 | ] 149 | 150 | [[package]] 151 | name = "mccabe" 152 | version = "0.7.0" 153 | description = "McCabe checker, plugin for flake8" 154 | optional = false 155 | python-versions = ">=3.6" 156 | files = [ 157 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 158 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 159 | ] 160 | 161 | [[package]] 162 | name = "packaging" 163 | version = "24.0" 164 | description = "Core utilities for Python packages" 165 | optional = false 166 | python-versions = ">=3.7" 167 | files = [ 168 | {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, 169 | {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, 170 | ] 171 | 172 | [[package]] 173 | name = "pluggy" 174 | version = "1.2.0" 175 | description = "plugin and hook calling mechanisms for python" 176 | optional = false 177 | python-versions = ">=3.7" 178 | files = [ 179 | {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, 180 | {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, 181 | ] 182 | 183 | [package.dependencies] 184 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 185 | 186 | [package.extras] 187 | dev = ["pre-commit", "tox"] 188 | testing = ["pytest", "pytest-benchmark"] 189 | 190 | [[package]] 191 | name = "pretend" 192 | version = "1.0.8" 193 | description = "A library for stubbing in Python" 194 | optional = false 195 | python-versions = "*" 196 | files = [ 197 | {file = "pretend-1.0.8-py2.py3-none-any.whl", hash = "sha256:a05f1d5eefad9f1b5471795d993e2e3ee25ebb82c273ca461710ed04d22dc07f"}, 198 | {file = "pretend-1.0.8.tar.gz", hash = "sha256:930f2c1e18503e8f8c403abe2e02166c4a881941745147e712cdd4f49f3fb964"}, 199 | ] 200 | 201 | [[package]] 202 | name = "pycodestyle" 203 | version = "2.9.1" 204 | description = "Python style guide checker" 205 | optional = false 206 | python-versions = ">=3.6" 207 | files = [ 208 | {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, 209 | {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, 210 | ] 211 | 212 | [[package]] 213 | name = "pyflakes" 214 | version = "2.5.0" 215 | description = "passive checker of Python programs" 216 | optional = false 217 | python-versions = ">=3.6" 218 | files = [ 219 | {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, 220 | {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, 221 | ] 222 | 223 | [[package]] 224 | name = "pyproject-flake8" 225 | version = "5.0.3" 226 | description = "pyproject-flake8 (`pflake8`), a monkey patching wrapper to connect flake8 with pyproject.toml configuration" 227 | optional = false 228 | python-versions = "*" 229 | files = [ 230 | {file = "pyproject-flake8-5.0.3.tar.gz", hash = "sha256:ed7b579431939b028a57561303cd1e08a0df81f6a7f4286f7ecb040203e0ec85"}, 231 | {file = "pyproject_flake8-5.0.3-py2.py3-none-any.whl", hash = "sha256:961d64891c2aef8f2128fd141ef08fcb43e34ff35ffe586a129d29825fc8ed86"}, 232 | ] 233 | 234 | [package.dependencies] 235 | flake8 = "5.0.3" 236 | tomli = {version = "*", markers = "python_version < \"3.11\""} 237 | 238 | [[package]] 239 | name = "pytest" 240 | version = "7.4.3" 241 | description = "pytest: simple powerful testing with Python" 242 | optional = false 243 | python-versions = ">=3.7" 244 | files = [ 245 | {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, 246 | {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, 247 | ] 248 | 249 | [package.dependencies] 250 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 251 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 252 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 253 | iniconfig = "*" 254 | packaging = "*" 255 | pluggy = ">=0.12,<2.0" 256 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 257 | 258 | [package.extras] 259 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 260 | 261 | [[package]] 262 | name = "tomli" 263 | version = "2.0.1" 264 | description = "A lil' TOML parser" 265 | optional = false 266 | python-versions = ">=3.7" 267 | files = [ 268 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 269 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 270 | ] 271 | 272 | [[package]] 273 | name = "typing-extensions" 274 | version = "4.7.1" 275 | description = "Backported and Experimental Type Hints for Python 3.7+" 276 | optional = false 277 | python-versions = ">=3.7" 278 | files = [ 279 | {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, 280 | {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, 281 | ] 282 | 283 | [[package]] 284 | name = "zipp" 285 | version = "3.15.0" 286 | description = "Backport of pathlib-compatible object wrapper for zip files" 287 | optional = false 288 | python-versions = ">=3.7" 289 | files = [ 290 | {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, 291 | {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, 292 | ] 293 | 294 | [package.extras] 295 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 296 | testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 297 | 298 | [metadata] 299 | lock-version = "2.0" 300 | python-versions = "^3.7" 301 | content-hash = "4dd581cf15cac499c22b463d1a96d7d3c3e7551f55ecebd916483475a7cc75b5" 302 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pike" 3 | version = "0.2.0" 4 | description = "Lightweight plugin management system for Python" 5 | authors = ["John Vrbanac "] 6 | license = "Apache v2" 7 | readme = "README.rst" 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.7" 11 | 12 | 13 | [tool.poetry.group.dev.dependencies] 14 | pytest = "<7.4.4" 15 | pretend = "<1.0.9" 16 | flake8 = "<5.0.4" 17 | coverage = "<7.6.12" 18 | pyproject-flake8 = "<7.0" 19 | 20 | [tool.flake8] 21 | max-line-length = 100 22 | 23 | [build-system] 24 | requires = ["poetry-core"] 25 | build-backend = "poetry.core.masonry.api" 26 | 27 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyarmory/pike/d2094b565044d9d2f6998b831aeecc5546d645e7/requirements.txt -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pretend 3 | flake8 4 | pyproject-flake8<7.0.0 5 | coverage 6 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyarmory/pike/d2094b565044d9d2f6998b831aeecc5546d645e7/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | import textwrap 4 | from py.path import local 5 | 6 | from pike.finder import PikeFinder 7 | 8 | 9 | # Default test dir: 10 | PIKE_TEST_DIR = 'pike_tests' 11 | 12 | 13 | # ------------------------------------------------------------ 14 | # Fixtures 15 | # 16 | # See http://pytest.org/latest/tmpdir.html 17 | 18 | @pytest.fixture 19 | def pike_finder(): 20 | """Fixture: Return PikeFinder object 21 | 22 | :return: PikeFinder object 23 | """ 24 | finder_path = local(__file__).dirpath().dirpath() 25 | return PikeFinder([str(finder_path)]) 26 | 27 | 28 | @pytest.fixture 29 | def pike_init_py(tmpdir): 30 | """Fixture: Create a directory with an empty __init__.py 31 | inside a temporary directory 32 | 33 | :param tmpdir: fixture with py.path.local object 34 | :return: temporary directory path to __init__.py 35 | 36 | HINT: Usually you can find the temporary directory under 37 | /tmp/pytest-of-$USER/pytest-$NUMBER/$NAME_OF_TEST_FUNCTION/ 38 | """ 39 | pkgdir = tmpdir.mkdir(PIKE_TEST_DIR) 40 | (pkgdir / "__init__.py").write("") 41 | return pkgdir 42 | 43 | 44 | @pytest.fixture 45 | def pike_tmp_package(pike_init_py): 46 | """Fixture: Create a Python package inside a temporary directory. 47 | Depends on the pike_init_py fixture 48 | 49 | :param pike_init_py: fixture with py.path.local object pointing to 50 | directory with empty __init__.py 51 | :return: temporary directory path to the package (containing 52 | __init__.py, app.py, and more.py) 53 | 54 | HINT: Usually you can find the temporary directory under 55 | /tmp/pytest-of-$USER/pytest-$NUMBER/$NAME_OF_TEST_FUNCTION/ 56 | """ 57 | # First file: 58 | (pike_init_py / 'app.py').write(textwrap.dedent(""" 59 | class SampleObj(object): 60 | pass 61 | 62 | class OtherObj(SampleObj): 63 | pass 64 | """)) 65 | 66 | # Second file: 67 | (pike_init_py / 'more.py').write(textwrap.dedent(""" 68 | class AnotherObj(object): 69 | pass 70 | """)) 71 | return pike_init_py 72 | -------------------------------------------------------------------------------- /tests/discovery/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyarmory/pike/d2094b565044d9d2f6998b831aeecc5546d645e7/tests/discovery/__init__.py -------------------------------------------------------------------------------- /tests/discovery/test_filesystem.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pike.discovery import filesystem 4 | from tests import utils 5 | 6 | 7 | class TestFilesystemDiscovery(object): 8 | def setup_method(self, method): 9 | self.temp_folder = utils.make_tmpdir() 10 | 11 | def teardown_method(self, method): 12 | utils.remove_dir(self.temp_folder) 13 | 14 | def test_find_modules(self): 15 | pkg_location = utils.create_working_package(self.temp_folder) 16 | mod_location = os.path.join(pkg_location, 'app.py') 17 | utils.write_file(mod_location) 18 | 19 | assert len(list(filesystem.find_modules(pkg_location))) == 1 20 | 21 | def test_find_modules_in_empty_package(self): 22 | pkg_location = utils.create_working_package(self.temp_folder) 23 | assert len(list(filesystem.find_modules(pkg_location))) == 0 24 | 25 | def test_find_packages(self): 26 | utils.create_working_package(self.temp_folder) 27 | assert len(list(filesystem.find_packages(self.temp_folder))) == 1 28 | 29 | def test_find_packages_in_empty_folder(self): 30 | assert len(list(filesystem.find_packages(self.temp_folder))) == 0 31 | 32 | def test_recursive_find_packages(self): 33 | pkg_location = utils.create_working_package(self.temp_folder) 34 | utils.create_working_package(pkg_location, 'bam') 35 | 36 | pkgs = filesystem.recursive_find_packages(self.temp_folder) 37 | 38 | assert len(list(pkgs)) == 2 39 | 40 | def test_recursive_find_modules(self): 41 | pkg_location = utils.create_working_package(self.temp_folder) 42 | subpkg_location = utils.create_working_package(pkg_location, 'bam') 43 | 44 | mod_location = os.path.join(pkg_location, 'app.py') 45 | utils.write_file(mod_location) 46 | mod_location = os.path.join(subpkg_location, 'other.py') 47 | utils.write_file(mod_location) 48 | 49 | pkgs = filesystem.recursive_find_modules(pkg_location) 50 | 51 | assert len(list(pkgs)) == 2 52 | -------------------------------------------------------------------------------- /tests/discovery/test_py.py: -------------------------------------------------------------------------------- 1 | import os 2 | import textwrap 3 | from pretend import stub 4 | 5 | from pike.discovery import py 6 | from pike.manager import PikeManager 7 | from tests import utils 8 | 9 | 10 | class BaseTestCase(object): 11 | def setup_method(self, method): 12 | self.temp_folder = utils.make_tmpdir() 13 | self.manager = PikeManager([self.temp_folder]) 14 | self.pkg_name = 'pike_{}'.format(method.__name__) 15 | self.pkg_location = utils.create_working_package( 16 | self.temp_folder, 17 | self.pkg_name 18 | ) 19 | 20 | def teardown_method(self, method): 21 | self.manager.cleanup() 22 | utils.remove_dir(self.temp_folder) 23 | 24 | 25 | class TestPyDiscovery(BaseTestCase): 26 | def test_get_module_by_name(self): 27 | assert py.get_module_by_name(self.pkg_name) is not None 28 | 29 | def test_is_child_of_module(self): 30 | child = stub(__name__='sample.child') 31 | parent = stub(__name__='sample') 32 | 33 | assert py.is_child_of_module(child, parent) 34 | 35 | def test_import_from_path(self): 36 | mod_location = os.path.join(self.pkg_location, 'app.py') 37 | utils.write_file(mod_location) 38 | 39 | assert py._import_from_path(mod_location, self.pkg_name) is not None 40 | 41 | def test_child_modules_without_sub_packages(self): 42 | mod_location = os.path.join(self.pkg_location, 'app.py') 43 | utils.write_file(mod_location) 44 | 45 | parent_module = py.get_module_by_name(self.pkg_name) 46 | child_modules = list(py._child_modules(parent_module)) 47 | 48 | assert len(child_modules) == 1 49 | 50 | def test_child_modules_with_sub_packages(self): 51 | subpkg_location = utils.create_working_package( 52 | self.pkg_location, 53 | 'submod' 54 | ) 55 | 56 | mod_location = os.path.join(self.pkg_location, 'app.py') 57 | utils.write_file(mod_location) 58 | submod_location = os.path.join(subpkg_location, 'other.py') 59 | utils.write_file(submod_location) 60 | 61 | parent_module = py.get_module_by_name(self.pkg_name) 62 | child_modules = list(py._child_modules(parent_module)) 63 | 64 | assert len(child_modules) == 2 65 | 66 | 67 | class TestDiscoverClasses(BaseTestCase): 68 | def setup_method(self, method): 69 | super(TestDiscoverClasses, self).setup_method(method) 70 | self.test_file_content = textwrap.dedent(""" 71 | class SampleObj(object): 72 | pass 73 | 74 | class OtherObj(SampleObj): 75 | pass 76 | """) 77 | 78 | mod_location = os.path.join(self.pkg_location, 'app.py') 79 | utils.write_file(mod_location, self.test_file_content) 80 | 81 | def test_classes_in_module(self): 82 | module = py.get_module_by_name('{}.app'.format(self.pkg_name)) 83 | assert len(list(py.classes_in_module(module))) == 2 84 | 85 | def test_get_all_classes(self): 86 | module = py.get_module_by_name(self.pkg_name) 87 | assert len(list(py.get_all_classes(module))) == 2 88 | 89 | def test_get_child_modules(self): 90 | subpkg_location = utils.create_working_package( 91 | self.pkg_location, 92 | 'sub_mod' 93 | ) 94 | mod_location = os.path.join(subpkg_location, 'something.py') 95 | utils.write_file(mod_location) 96 | 97 | module = py.get_module_by_name(self.pkg_name) 98 | # Recursive 99 | assert len(list(py.get_child_modules(module))) == 3 100 | 101 | # Non-Recursive 102 | assert len(list(py.get_child_modules(module, False))) == 2 103 | 104 | def test_get_all_inherited_classes(self): 105 | module = py.get_module_by_name('{}.app'.format(self.pkg_name)) 106 | test_base_class = getattr(module, 'SampleObj') 107 | 108 | classes = py.get_all_inherited_classes(module, test_base_class) 109 | assert len(classes) == 1 110 | 111 | def test_get_inherited_classes(self): 112 | module = py.get_module_by_name('{}.app'.format(self.pkg_name)) 113 | test_base_class = getattr(module, 'SampleObj') 114 | 115 | classes = py.get_inherited_classes(module, test_base_class) 116 | assert len(classes) == 1 117 | -------------------------------------------------------------------------------- /tests/test_finder.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def test_module_name_to_filename(pike_finder): 5 | res = pike_finder.module_name_to_filename('pike.finder') 6 | assert res == 'pike{0}finder'.format(os.path.sep) 7 | 8 | 9 | def test_get_import_filename_module(pike_finder): 10 | filename = pike_finder.module_name_to_filename('tests.test_finder') 11 | module_path = pike_finder.get_import_filename(filename) 12 | assert module_path == __file__ 13 | 14 | 15 | def test_get_import_filename_package(pike_finder): 16 | filename = pike_finder.module_name_to_filename('tests') 17 | module_path = pike_finder.get_import_filename(filename) 18 | 19 | assert module_path.endswith('tests{0}__init__.py'.format(os.path.sep)) 20 | 21 | 22 | def test_no_loader_returned_if_module_not_in_scope(pike_finder): 23 | loader = pike_finder.find_module('bam') 24 | assert not loader 25 | -------------------------------------------------------------------------------- /tests/test_loader.py: -------------------------------------------------------------------------------- 1 | import os 2 | import py_compile 3 | 4 | import pytest 5 | from pike.finder import PikeFinder 6 | from pike.loader import PikeLoader 7 | from tests import utils 8 | 9 | 10 | SIMPLE_CLASS = """ 11 | class Tracer(object): 12 | pass 13 | """ 14 | 15 | 16 | @pytest.fixture 17 | def loader_finder(): 18 | temp_folder = utils.make_tmpdir() 19 | 20 | # Create a simple package 21 | pkg_location = utils.create_working_package(temp_folder) 22 | mod_location = os.path.join(pkg_location, 'app.py') 23 | utils.write_file(mod_location, SIMPLE_CLASS) 24 | 25 | finder = PikeFinder([temp_folder]) 26 | loader = PikeLoader('pike_tests.app', mod_location) 27 | 28 | yield loader, finder 29 | 30 | utils.remove_dir(temp_folder) 31 | 32 | 33 | @pytest.fixture 34 | def compiled_loader(): 35 | temp_folder = utils.make_tmpdir() 36 | 37 | # Create a simple package 38 | pkg_location = utils.create_working_package(temp_folder, 'compile_test') 39 | mod_location = os.path.join(pkg_location, 'app.py') 40 | utils.write_file(mod_location, SIMPLE_CLASS) 41 | 42 | py_compile.compile(mod_location) 43 | 44 | yield temp_folder 45 | 46 | utils.remove_dir(temp_folder) 47 | 48 | 49 | def test_load_module_raises_import_error_with_bad_fullname(loader_finder): 50 | loader, _ = loader_finder 51 | 52 | with pytest.raises(ImportError): 53 | loader.load_module('bam') 54 | 55 | 56 | def test_is_package(loader_finder): 57 | _, finder = loader_finder 58 | 59 | loader = finder.find_module('pike_tests') 60 | assert loader.is_package() 61 | 62 | 63 | def test_module_isnt_package(loader_finder): 64 | _, finder = loader_finder 65 | 66 | loader = finder.find_module('pike_tests.app') 67 | assert not loader.is_package() 68 | 69 | 70 | def test_load_package_module(loader_finder): 71 | _, finder = loader_finder 72 | 73 | loader = finder.find_module('pike_tests') 74 | module = loader.load_module('pike_tests') 75 | assert module is not None 76 | 77 | 78 | def test_second_load_pulls_previously_loaded_module(loader_finder): 79 | loader, _ = loader_finder 80 | 81 | first_load = loader.load_module('pike_tests.app') 82 | second_load = loader.load_module('pike_tests.app') 83 | assert first_load == second_load 84 | 85 | 86 | def test_load_module_by_path_with_invalid_path(loader_finder): 87 | loader, _ = loader_finder 88 | 89 | module = loader.load_module_by_path('name', 'something.bam') 90 | assert module is None 91 | 92 | 93 | @pytest.mark.skip('pyc loading is disabled') 94 | def test_loading_pyc(compiled_loader): 95 | finder = PikeFinder([compiled_loader]) 96 | 97 | # Loading compiled module 98 | loader = finder.find_module('compile_test.app') 99 | module = loader.load_module('compile_test.app') 100 | 101 | assert type(module.__loader__).__name__ == 'SourcelessFileLoader' 102 | assert module.__cached__.endswith('.pyc') 103 | 104 | 105 | def test_loading_py(compiled_loader): 106 | finder = PikeFinder([compiled_loader]) 107 | 108 | # Loading module source 109 | loader = finder.find_module('compile_test') 110 | module = loader.load_module('compile_test') 111 | 112 | assert type(module.__loader__).__name__ == 'SourceFileLoader' 113 | -------------------------------------------------------------------------------- /tests/test_manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import textwrap 4 | 5 | from pike.manager import PikeManager 6 | from pike.discovery import py 7 | from tests import utils 8 | 9 | 10 | def finders_in_meta_path(): 11 | return [finder for finder in sys.meta_path 12 | if type(finder).__name__ == 'PikeFinder'] 13 | 14 | 15 | def test_manager_with_normal_instantiation(): 16 | mgr = PikeManager(['./']) 17 | assert mgr.module_finder in sys.meta_path 18 | 19 | mgr.cleanup() 20 | assert mgr.module_finder not in sys.meta_path 21 | 22 | 23 | def test_manager_as_context_manager(): 24 | mgr = PikeManager(['./']) 25 | with mgr: 26 | assert mgr.module_finder in sys.meta_path 27 | assert mgr.module_finder not in sys.meta_path 28 | 29 | 30 | def test_double_add_meta_path(): 31 | with PikeManager(['./']) as mgr: 32 | mgr.add_to_meta_path() 33 | assert len(finders_in_meta_path()) == 1 34 | 35 | 36 | def test_del_removes_from_meta_path(): 37 | mgr = PikeManager(['./']) 38 | assert mgr.module_finder in sys.meta_path 39 | 40 | mgr.__del__() 41 | assert len(finders_in_meta_path()) == 0 42 | 43 | 44 | def test_double_cleanup_shouldnt_fail(): 45 | """Making sure multiple cleanups don't cause problems 46 | 47 | This case happens when Python's GC calls the destructor on the manager 48 | """ 49 | mgr = PikeManager(['./']) 50 | mgr.cleanup() 51 | mgr.cleanup() 52 | assert mgr.module_finder not in sys.meta_path 53 | 54 | 55 | def test_get_classes(): 56 | temp_folder = utils.make_tmpdir() 57 | pkg_name = 'pike_mgr_classes' 58 | pkg_location = utils.create_working_package(temp_folder, pkg_name) 59 | 60 | test_file_content = textwrap.dedent(""" 61 | class SampleObj(object): 62 | pass 63 | 64 | class OtherObj(SampleObj): 65 | pass 66 | """) 67 | 68 | mod_location = os.path.join(pkg_location, 'app.py') 69 | utils.write_file(mod_location, test_file_content) 70 | 71 | # Include module directly on the search path 72 | second_file = textwrap.dedent(""" 73 | class AnotherObj(object): 74 | pass 75 | """) 76 | mod_location = os.path.join(temp_folder, 'more.py') 77 | utils.write_file(mod_location, second_file) 78 | 79 | classes = [] 80 | with PikeManager([temp_folder]) as mgr: 81 | classes = mgr.get_classes() 82 | 83 | assert len(classes) == 3 84 | 85 | 86 | def test_get_classes_with_fixtures(pike_tmp_package): 87 | """Structurally the same than test_get_classes, but with fixtures 88 | (see conftest.py) 89 | """ 90 | classes = [] 91 | with PikeManager([str(pike_tmp_package)]) as mgr: 92 | classes = mgr.get_classes() 93 | 94 | assert len(classes) == 3 95 | 96 | 97 | def test_get_inherited_classes(): 98 | temp_folder = utils.make_tmpdir() 99 | pkg_name = 'pike_mgr_inherited_classes' 100 | pkg_location = utils.create_working_package(temp_folder, pkg_name) 101 | 102 | test_file_content = textwrap.dedent(""" 103 | class SampleObj(object): 104 | pass 105 | 106 | class OtherObj(SampleObj): 107 | pass 108 | """) 109 | 110 | mod_location = os.path.join(pkg_location, 'app.py') 111 | utils.write_file(mod_location, test_file_content) 112 | 113 | # Include module directly on the search path 114 | second_file = textwrap.dedent(""" 115 | class AnotherObj(object): 116 | pass 117 | """) 118 | mod_location = os.path.join(temp_folder, 'more.py') 119 | utils.write_file(mod_location, second_file) 120 | 121 | classes = [] 122 | with PikeManager([temp_folder]) as mgr: 123 | app = py.get_module_by_name('{}.app'.format(pkg_name)) 124 | classes = mgr.get_all_inherited_classes(app.SampleObj) 125 | 126 | assert len(classes) == 1 127 | 128 | 129 | def test_get_inherited_classes_with_fixtures(pike_tmp_package): 130 | """Structurally the same than test_get_inherited_classes, but with fixtures 131 | (see conftest.py) 132 | """ 133 | # Actually, this is not really needed. 134 | pkg_name = 'pike_mgr_inherited_classes' 135 | pike_tmp_package.rename(pike_tmp_package.dirpath() / pkg_name) 136 | classes = [] 137 | with PikeManager([pike_tmp_package.dirname]) as mgr: 138 | app = py.get_module_by_name('{}.app'.format(pkg_name)) 139 | classes = mgr.get_all_inherited_classes(app.SampleObj) 140 | 141 | assert len(classes) == 1 142 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | 5 | 6 | def make_tmpdir(): 7 | return tempfile.mkdtemp() 8 | 9 | 10 | def remove_dir(filename): 11 | if os.path.exists(filename): 12 | shutil.rmtree(filename) 13 | 14 | 15 | def write_file(filename, content=''): 16 | with open(filename, 'w') as fp: 17 | fp.write(content) 18 | 19 | 20 | def create_working_package(location, name=None): 21 | pkg_location = os.path.join(location, name or 'pike_tests') 22 | os.makedirs(pkg_location) 23 | write_file(os.path.join(pkg_location, '__init__.py')) 24 | 25 | return pkg_location 26 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = flake8, py37, py38, py39, py310, py311, py312, pypy, coverage 3 | isolated_build = True 4 | 5 | [testenv] 6 | deps = -r{toxinidir}/test-requirements.txt 7 | commands = 8 | coverage run --parallel-mode -m pytest {toxinidir}/tests 9 | 10 | [testenv:coverage] 11 | deps = coverage 12 | commands = 13 | coverage combine 14 | coverage report -m 15 | coverage erase 16 | 17 | [testenv:flake8] 18 | commands = 19 | pflake8 --statistics -j auto --count pike tests 20 | --------------------------------------------------------------------------------