├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.rst ├── doc ├── Makefile ├── api.rst ├── conf.py └── index.rst ├── entrypoints.py ├── pyproject.toml └── tests ├── __init__.py ├── samples ├── packages1 │ ├── baz-0.3.egg │ │ └── EGG-INFO │ │ │ └── entry_points.txt │ └── foo-0.1.dist-info │ │ └── entry_points.txt ├── packages2 │ ├── bar-0.2.egg-info │ │ └── entry_points.txt │ ├── foo-0.1.1.dist-info │ │ └── entry_points.txt │ ├── qux-0.4.egg │ └── qux-0.4.egg.unpacked │ │ └── EGG-INFO │ │ └── entry_points.txt ├── packages3 │ └── foo-0.1.dist-info │ │ └── entry_points.txt └── packages4 │ └── dev.egg-info │ └── entry_points.txt └── test_entrypoints.py /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [ "3.6", "3.7", "3.8", "3.9", "3.10" ] 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Setup Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip wheel 22 | pip install . pytest 23 | 24 | - name: Run tests 25 | run: pytest 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.pyc 3 | /build/ 4 | /dist/ 5 | /doc/_build/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Thomas Kluyver and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **This package is in maintenance-only mode.** New code should use the 2 | `importlib.metadata module `_ 3 | in the Python standard library to find and load entry points. 4 | 5 | Entry points are a way for Python packages to advertise objects with some 6 | common interface. The most common examples are ``console_scripts`` entry points, 7 | which define shell commands by identifying a Python function to run. 8 | 9 | *Groups* of entry points, such as ``console_scripts``, point to objects with 10 | similar interfaces. An application might use a group to find its plugins, or 11 | multiple groups if it has different kinds of plugins. 12 | 13 | The **entrypoints** module contains functions to find and load entry points. 14 | You can install it from PyPI with ``pip install entrypoints``. 15 | 16 | To advertise entry points when distributing a package, see 17 | `entry_points in the Python Packaging User Guide 18 | `_. 19 | 20 | The ``pkg_resources`` module distributed with ``setuptools`` provides a way to 21 | discover entrypoints as well, but it contains other functionality unrelated to 22 | entrypoint discovery, and it does a lot of work at import time. Merely 23 | *importing* ``pkg_resources`` causes it to scan the files of all installed 24 | packages. Thus, in environments where a large number of packages are installed, 25 | importing ``pkg_resources`` can be very slow (several seconds). 26 | 27 | By contrast, ``entrypoints`` is focused solely on entrypoint discovery and it 28 | is faster. Importing ``entrypoints`` does not scan anything, and getting a 29 | given entrypoint group performs a more focused scan. 30 | 31 | When there are multiple versions of the same distribution in different 32 | directories on ``sys.path``, ``entrypoints`` follows the rule that the first 33 | one wins. In most cases, this follows the logic of imports. Similarly, 34 | Entrypoints relies on ``pip`` to ensure that only one ``.dist-info`` or 35 | ``.egg-info`` directory exists for each installed package. There is no reliable 36 | way to pick which of several `.dist-info` folders accurately relates to the 37 | importable modules. -------------------------------------------------------------------------------- /doc/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/entrypoints.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/entrypoints.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/entrypoints" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/entrypoints" 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 | -------------------------------------------------------------------------------- /doc/api.rst: -------------------------------------------------------------------------------- 1 | entrypoints API 2 | =============== 3 | 4 | .. module:: entrypoints 5 | 6 | High-level API 7 | -------------- 8 | 9 | .. autofunction:: get_single 10 | 11 | .. autofunction:: get_group_named 12 | 13 | .. autofunction:: get_group_all 14 | 15 | These functions will all use ``sys.path`` by default if you don't specify the 16 | *path* parameter. This is normally what you want, so you shouldn't need to 17 | pass *path*. 18 | 19 | EntryPoint objects 20 | ------------------ 21 | 22 | .. autoclass:: EntryPoint 23 | 24 | .. attribute:: name 25 | 26 | The name identifying this entry point 27 | 28 | .. attribute:: module_name 29 | 30 | The name of an importable module to which it refers 31 | 32 | .. attribute:: object_name 33 | 34 | The dotted object name within the module, or *None* if the entry point 35 | refers to a module itself. 36 | 37 | .. attribute:: extras 38 | 39 | Extra setuptools features related to this entry point as a list, or *None* 40 | 41 | .. attribute:: distro 42 | 43 | The distribution which advertised this entry point - 44 | a :class:`Distribution` instance or None 45 | 46 | .. automethod:: load 47 | 48 | .. automethod:: from_string 49 | 50 | .. autoclass:: Distribution 51 | 52 | .. attribute:: name 53 | 54 | The name of this distribution 55 | 56 | .. attribute:: version 57 | 58 | The version of this distribution, as a string 59 | 60 | Exceptions 61 | ---------- 62 | 63 | .. autoexception:: BadEntryPoint 64 | 65 | .. autoexception:: NoSuchEntryPoint 66 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # entrypoints documentation build configuration file, created by 5 | # sphinx-quickstart on Fri May 8 11:50:43 2015. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | import shlex 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | 24 | sys.path.insert(0, os.path.abspath('..')) 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | #needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.intersphinx', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix(es) of source filenames. 43 | # You can specify multiple suffix as a list of string: 44 | # source_suffix = ['.rst', '.md'] 45 | source_suffix = '.rst' 46 | 47 | # The encoding of source files. 48 | #source_encoding = 'utf-8-sig' 49 | 50 | # The master toctree document. 51 | master_doc = 'index' 52 | 53 | # General information about the project. 54 | project = 'entrypoints' 55 | copyright = '2015, Thomas Kluyver' 56 | author = 'Thomas Kluyver' 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The short X.Y version. 63 | from entrypoints import __version__ as version 64 | # The full version, including alpha/beta/rc tags. 65 | release = version 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = None 73 | 74 | # There are two options for replacing |today|: either, you set today to some 75 | # non-false value, then it is used: 76 | #today = '' 77 | # Else, today_fmt is used as the format for a strftime call. 78 | #today_fmt = '%B %d, %Y' 79 | 80 | # List of patterns, relative to source directory, that match files and 81 | # directories to ignore when looking for source files. 82 | exclude_patterns = ['_build'] 83 | 84 | # The reST default role (used for this markup: `text`) to use for all 85 | # documents. 86 | #default_role = None 87 | 88 | # If true, '()' will be appended to :func: etc. cross-reference text. 89 | #add_function_parentheses = True 90 | 91 | # If true, the current module name will be prepended to all description 92 | # unit titles (such as .. function::). 93 | #add_module_names = True 94 | 95 | # If true, sectionauthor and moduleauthor directives will be shown in the 96 | # output. They are ignored by default. 97 | #show_authors = False 98 | 99 | # The name of the Pygments (syntax highlighting) style to use. 100 | pygments_style = 'sphinx' 101 | 102 | # A list of ignored prefixes for module index sorting. 103 | #modindex_common_prefix = [] 104 | 105 | # If true, keep warnings as "system message" paragraphs in the built documents. 106 | #keep_warnings = False 107 | 108 | # If true, `todo` and `todoList` produce output, else they produce nothing. 109 | todo_include_todos = False 110 | 111 | 112 | # -- Options for HTML output ---------------------------------------------- 113 | 114 | # The theme to use for HTML and HTML Help pages. See the documentation for 115 | # a list of builtin themes. 116 | # html_theme = 'alabaster' 117 | 118 | # Theme options are theme-specific and customize the look and feel of a theme 119 | # further. For a list of options available for each theme, see the 120 | # documentation. 121 | #html_theme_options = {} 122 | 123 | # Add any paths that contain custom themes here, relative to this directory. 124 | #html_theme_path = [] 125 | 126 | # The name for this set of Sphinx documents. If None, it defaults to 127 | # " v documentation". 128 | #html_title = None 129 | 130 | # A shorter title for the navigation bar. Default is the same as html_title. 131 | #html_short_title = None 132 | 133 | # The name of an image file (relative to this directory) to place at the top 134 | # of the sidebar. 135 | #html_logo = None 136 | 137 | # The name of an image file (within the static path) to use as favicon of the 138 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 139 | # pixels large. 140 | #html_favicon = None 141 | 142 | # Add any paths that contain custom static files (such as style sheets) here, 143 | # relative to this directory. They are copied after the builtin static files, 144 | # so a file named "default.css" will overwrite the builtin "default.css". 145 | html_static_path = ['_static'] 146 | 147 | # Add any extra paths that contain custom files (such as robots.txt or 148 | # .htaccess) here, relative to this directory. These files are copied 149 | # directly to the root of the documentation. 150 | #html_extra_path = [] 151 | 152 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 153 | # using the given strftime format. 154 | #html_last_updated_fmt = '%b %d, %Y' 155 | 156 | # If true, SmartyPants will be used to convert quotes and dashes to 157 | # typographically correct entities. 158 | #html_use_smartypants = True 159 | 160 | # Custom sidebar templates, maps document names to template names. 161 | #html_sidebars = {} 162 | 163 | # Additional templates that should be rendered to pages, maps page names to 164 | # template names. 165 | #html_additional_pages = {} 166 | 167 | # If false, no module index is generated. 168 | #html_domain_indices = True 169 | 170 | # If false, no index is generated. 171 | #html_use_index = True 172 | 173 | # If true, the index is split into individual pages for each letter. 174 | #html_split_index = False 175 | 176 | # If true, links to the reST sources are added to the pages. 177 | #html_show_sourcelink = True 178 | 179 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 180 | #html_show_sphinx = True 181 | 182 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 183 | #html_show_copyright = True 184 | 185 | # If true, an OpenSearch description file will be output, and all pages will 186 | # contain a tag referring to it. The value of this option must be the 187 | # base URL from which the finished HTML is served. 188 | #html_use_opensearch = '' 189 | 190 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 191 | #html_file_suffix = None 192 | 193 | # Language to be used for generating the HTML full-text search index. 194 | # Sphinx supports the following languages: 195 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 196 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 197 | #html_search_language = 'en' 198 | 199 | # A dictionary with options for the search language support, empty by default. 200 | # Now only 'ja' uses this config value 201 | #html_search_options = {'type': 'default'} 202 | 203 | # The name of a javascript file (relative to the configuration directory) that 204 | # implements a search results scorer. If empty, the default will be used. 205 | #html_search_scorer = 'scorer.js' 206 | 207 | # Output file base name for HTML help builder. 208 | htmlhelp_basename = 'entrypointsdoc' 209 | 210 | # -- Options for LaTeX output --------------------------------------------- 211 | 212 | latex_elements = { 213 | # The paper size ('letterpaper' or 'a4paper'). 214 | #'papersize': 'letterpaper', 215 | 216 | # The font size ('10pt', '11pt' or '12pt'). 217 | #'pointsize': '10pt', 218 | 219 | # Additional stuff for the LaTeX preamble. 220 | #'preamble': '', 221 | 222 | # Latex figure (float) alignment 223 | #'figure_align': 'htbp', 224 | } 225 | 226 | # Grouping the document tree into LaTeX files. List of tuples 227 | # (source start file, target name, title, 228 | # author, documentclass [howto, manual, or own class]). 229 | latex_documents = [ 230 | (master_doc, 'entrypoints.tex', 'entrypoints Documentation', 231 | 'Thomas Kluyver', 'manual'), 232 | ] 233 | 234 | # The name of an image file (relative to this directory) to place at the top of 235 | # the title page. 236 | #latex_logo = None 237 | 238 | # For "manual" documents, if this is true, then toplevel headings are parts, 239 | # not chapters. 240 | #latex_use_parts = False 241 | 242 | # If true, show page references after internal links. 243 | #latex_show_pagerefs = False 244 | 245 | # If true, show URL addresses after external links. 246 | #latex_show_urls = False 247 | 248 | # Documents to append as an appendix to all manuals. 249 | #latex_appendices = [] 250 | 251 | # If false, no module index is generated. 252 | #latex_domain_indices = True 253 | 254 | 255 | # -- Options for manual page output --------------------------------------- 256 | 257 | # One entry per manual page. List of tuples 258 | # (source start file, name, description, authors, manual section). 259 | man_pages = [ 260 | (master_doc, 'entrypoints', 'entrypoints Documentation', 261 | [author], 1) 262 | ] 263 | 264 | # If true, show URL addresses after external links. 265 | #man_show_urls = False 266 | 267 | 268 | # -- Options for Texinfo output ------------------------------------------- 269 | 270 | # Grouping the document tree into Texinfo files. List of tuples 271 | # (source start file, target name, title, author, 272 | # dir menu entry, description, category) 273 | texinfo_documents = [ 274 | (master_doc, 'entrypoints', 'entrypoints Documentation', 275 | author, 'entrypoints', 'One line description of project.', 276 | 'Miscellaneous'), 277 | ] 278 | 279 | # Documents to append as an appendix to all manuals. 280 | #texinfo_appendices = [] 281 | 282 | # If false, no module index is generated. 283 | #texinfo_domain_indices = True 284 | 285 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 286 | #texinfo_show_urls = 'footnote' 287 | 288 | # If true, do not generate a @detailmenu in the "Top" node's menu. 289 | #texinfo_no_detailmenu = False 290 | 291 | 292 | # Example configuration for intersphinx: refer to the Python standard library. 293 | intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} 294 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | entrypoints |version| 2 | ===================== 3 | 4 | .. include:: ../README.rst 5 | 6 | Contents: 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | api 12 | 13 | Indices and tables 14 | ================== 15 | 16 | * :ref:`genindex` 17 | * :ref:`modindex` 18 | * :ref:`search` 19 | 20 | -------------------------------------------------------------------------------- /entrypoints.py: -------------------------------------------------------------------------------- 1 | """Discover and load entry points from installed packages.""" 2 | # Copyright (c) Thomas Kluyver and contributors 3 | # Distributed under the terms of the MIT license; see LICENSE file. 4 | 5 | from contextlib import contextmanager 6 | import glob 7 | from importlib import import_module 8 | import io 9 | import itertools 10 | import os.path as osp 11 | import re 12 | import sys 13 | import warnings 14 | import zipfile 15 | 16 | import configparser 17 | 18 | entry_point_pattern = re.compile(r""" 19 | (?P\w+(\.\w+)*) 20 | (:(?P\w+(\.\w+)*))? 21 | \s* 22 | (\[(?P.+)\])? 23 | $ 24 | """, re.VERBOSE) 25 | 26 | file_in_zip_pattern = re.compile(r""" 27 | (?P[^/\\]+)\.(dist|egg)-info 28 | [/\\]entry_points.txt$ 29 | """, re.VERBOSE) 30 | 31 | __version__ = '0.4' 32 | 33 | class BadEntryPoint(Exception): 34 | """Raised when an entry point can't be parsed. 35 | """ 36 | def __init__(self, epstr): 37 | self.epstr = epstr 38 | 39 | def __str__(self): 40 | return "Couldn't parse entry point spec: %r" % self.epstr 41 | 42 | @staticmethod 43 | @contextmanager 44 | def err_to_warnings(): 45 | try: 46 | yield 47 | except BadEntryPoint as e: 48 | warnings.warn(str(e)) 49 | 50 | class NoSuchEntryPoint(Exception): 51 | """Raised by :func:`get_single` when no matching entry point is found.""" 52 | def __init__(self, group, name): 53 | self.group = group 54 | self.name = name 55 | 56 | def __str__(self): 57 | return "No {!r} entry point found in group {!r}".format(self.name, self.group) 58 | 59 | 60 | class CaseSensitiveConfigParser(configparser.ConfigParser): 61 | optionxform = staticmethod(str) 62 | 63 | 64 | class EntryPoint(object): 65 | def __init__(self, name, module_name, object_name, extras=None, distro=None): 66 | self.name = name 67 | self.module_name = module_name 68 | self.object_name = object_name 69 | self.extras = extras 70 | self.distro = distro 71 | 72 | def __repr__(self): 73 | return "EntryPoint(%r, %r, %r, %r)" % \ 74 | (self.name, self.module_name, self.object_name, self.distro) 75 | 76 | def load(self): 77 | """Load the object to which this entry point refers. 78 | """ 79 | mod = import_module(self.module_name) 80 | obj = mod 81 | if self.object_name: 82 | for attr in self.object_name.split('.'): 83 | obj = getattr(obj, attr) 84 | return obj 85 | 86 | @classmethod 87 | def from_string(cls, epstr, name, distro=None): 88 | """Parse an entry point from the syntax in entry_points.txt 89 | 90 | :param str epstr: The entry point string (not including 'name =') 91 | :param str name: The name of this entry point 92 | :param Distribution distro: The distribution in which the entry point was found 93 | :rtype: EntryPoint 94 | :raises BadEntryPoint: if *epstr* can't be parsed as an entry point. 95 | """ 96 | m = entry_point_pattern.match(epstr) 97 | if m: 98 | mod, obj, extras = m.group('modulename', 'objectname', 'extras') 99 | if extras is not None: 100 | extras = re.split(r',\s*', extras) 101 | return cls(name, mod, obj, extras, distro) 102 | else: 103 | raise BadEntryPoint(epstr) 104 | 105 | class Distribution(object): 106 | def __init__(self, name, version): 107 | self.name = name 108 | self.version = version 109 | 110 | @classmethod 111 | def from_name_version(cls, name): 112 | """Parse a distribution from a "name-version" string 113 | 114 | :param str name: The name-version string (entrypoints-0.3) 115 | Returns an :class:`Distribution` object 116 | """ 117 | version = None 118 | if '-' in name: 119 | name, version = name.split('-', 1) 120 | return cls(name, version) 121 | 122 | def __repr__(self): 123 | return "Distribution(%r, %r)" % (self.name, self.version) 124 | 125 | 126 | def iter_files_distros(path=None, repeated_distro='first'): 127 | if path is None: 128 | path = sys.path 129 | 130 | # Distributions found earlier in path will shadow those with the same name 131 | # found later. If these distributions used different module names, it may 132 | # actually be possible to import both, but in most cases this shadowing 133 | # will be correct. 134 | distro_names_seen = set() 135 | 136 | for folder in path: 137 | if folder.rstrip('/\\').endswith('.egg'): 138 | # Gah, eggs 139 | egg_name = osp.basename(folder) 140 | distro = Distribution.from_name_version(egg_name.split(".egg")[0]) 141 | 142 | if (repeated_distro == 'first') \ 143 | and (distro.name in distro_names_seen): 144 | continue 145 | distro_names_seen.add(distro.name) 146 | 147 | if osp.isdir(folder): 148 | ep_path = osp.join(folder, 'EGG-INFO', 'entry_points.txt') 149 | if osp.isfile(ep_path): 150 | cp = CaseSensitiveConfigParser(delimiters=('=',)) 151 | cp.read([ep_path]) 152 | yield cp, distro 153 | 154 | elif zipfile.is_zipfile(folder): 155 | z = zipfile.ZipFile(folder) 156 | try: 157 | info = z.getinfo('EGG-INFO/entry_points.txt') 158 | except KeyError: 159 | continue 160 | cp = CaseSensitiveConfigParser(delimiters=('=',)) 161 | with z.open(info) as f: 162 | fu = io.TextIOWrapper(f) 163 | cp.read_file(fu, source=osp.join( 164 | folder, 'EGG-INFO', 'entry_points.txt')) 165 | yield cp, distro 166 | 167 | # zip imports, not egg 168 | elif zipfile.is_zipfile(folder): 169 | with zipfile.ZipFile(folder) as zf: 170 | for info in zf.infolist(): 171 | m = file_in_zip_pattern.match(info.filename) 172 | if not m: 173 | continue 174 | 175 | distro_name_version = m.group('dist_version') 176 | distro = Distribution.from_name_version(distro_name_version) 177 | 178 | if (repeated_distro == 'first') \ 179 | and (distro.name in distro_names_seen): 180 | continue 181 | distro_names_seen.add(distro.name) 182 | 183 | cp = CaseSensitiveConfigParser(delimiters=('=',)) 184 | with zf.open(info) as f: 185 | fu = io.TextIOWrapper(f) 186 | cp.read_file(fu, source=osp.join(folder, info.filename)) 187 | yield cp, distro 188 | 189 | # Regular file imports (not egg, not zip file) 190 | for path in itertools.chain( 191 | glob.iglob(osp.join(glob.escape(folder), '*.dist-info', 'entry_points.txt')), 192 | glob.iglob(osp.join(glob.escape(folder), '*.egg-info', 'entry_points.txt')) 193 | ): 194 | distro_name_version = osp.splitext(osp.basename(osp.dirname(path)))[0] 195 | distro = Distribution.from_name_version(distro_name_version) 196 | 197 | if (repeated_distro == 'first') \ 198 | and (distro.name in distro_names_seen): 199 | continue 200 | distro_names_seen.add(distro.name) 201 | 202 | cp = CaseSensitiveConfigParser(delimiters=('=',)) 203 | cp.read([path]) 204 | yield cp, distro 205 | 206 | def get_single(group, name, path=None): 207 | """Find a single entry point. 208 | 209 | Returns an :class:`EntryPoint` object, or raises :exc:`NoSuchEntryPoint` 210 | if no match is found. 211 | """ 212 | for config, distro in iter_files_distros(path=path): 213 | if (group in config) and (name in config[group]): 214 | epstr = config[group][name] 215 | with BadEntryPoint.err_to_warnings(): 216 | return EntryPoint.from_string(epstr, name, distro) 217 | 218 | raise NoSuchEntryPoint(group, name) 219 | 220 | def get_group_named(group, path=None): 221 | """Find a group of entry points with unique names. 222 | 223 | Returns a dictionary of names to :class:`EntryPoint` objects. 224 | """ 225 | result = {} 226 | for ep in get_group_all(group, path=path): 227 | if ep.name not in result: 228 | result[ep.name] = ep 229 | return result 230 | 231 | def get_group_all(group, path=None): 232 | """Find all entry points in a group. 233 | 234 | Returns a list of :class:`EntryPoint` objects. 235 | """ 236 | result = [] 237 | for config, distro in iter_files_distros(path=path): 238 | if group in config: 239 | for name, epstr in config[group].items(): 240 | with BadEntryPoint.err_to_warnings(): 241 | result.append(EntryPoint.from_string(epstr, name, distro)) 242 | 243 | return result 244 | 245 | if __name__ == '__main__': 246 | import pprint 247 | pprint.pprint(get_group_all('console_scripts')) 248 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [tool.flit.metadata] 6 | module = "entrypoints" 7 | author = "Thomas Kluyver" 8 | author-email = "thomas@kluyver.me.uk" 9 | home-page = "https://github.com/takluyver/entrypoints" 10 | description-file = "README.rst" 11 | classifiers = [ 12 | "License :: OSI Approved :: MIT License", 13 | "Programming Language :: Python :: 3" 14 | ] 15 | requires-python = ">=3.6" 16 | 17 | [tool.flit.metadata.urls] 18 | Documentation = "https://entrypoints.readthedocs.io/en/latest/" 19 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takluyver/entrypoints/d943dd43a5f08391c49b4c5c1627d907c21e4384/tests/__init__.py -------------------------------------------------------------------------------- /tests/samples/packages1/baz-0.3.egg/EGG-INFO/entry_points.txt: -------------------------------------------------------------------------------- 1 | [entrypoints.test1] 2 | rew = baz:init 3 | -------------------------------------------------------------------------------- /tests/samples/packages1/foo-0.1.dist-info/entry_points.txt: -------------------------------------------------------------------------------- 1 | [entrypoints.test1] 2 | abc = foo:abc 3 | 4 | [blogtool.parsers] 5 | .rst = some.nested.module:SomeClass.some_classmethod [reST] 6 | 7 | [test.case_sensitive] 8 | Ptangle = pweave.scripts:tangle 9 | ptangle = pweave.scripts:tangle 10 | -------------------------------------------------------------------------------- /tests/samples/packages2/bar-0.2.egg-info/entry_points.txt: -------------------------------------------------------------------------------- 1 | [entrypoints.test1] 2 | opo = bar:main 3 | abc = bar:abc 4 | -------------------------------------------------------------------------------- /tests/samples/packages2/foo-0.1.1.dist-info/entry_points.txt: -------------------------------------------------------------------------------- 1 | [entrypoints.test1] 2 | dontseeme = invisible 3 | 4 | # This shouldn't be seen, because another foo distribution is found 5 | # in packages1, which is before this on the path. 6 | -------------------------------------------------------------------------------- /tests/samples/packages2/qux-0.4.egg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takluyver/entrypoints/d943dd43a5f08391c49b4c5c1627d907c21e4384/tests/samples/packages2/qux-0.4.egg -------------------------------------------------------------------------------- /tests/samples/packages2/qux-0.4.egg.unpacked/EGG-INFO/entry_points.txt: -------------------------------------------------------------------------------- 1 | [entrypoints.test1] 2 | njn = qux.extn:Njn.load 3 | -------------------------------------------------------------------------------- /tests/samples/packages3/foo-0.1.dist-info/entry_points.txt: -------------------------------------------------------------------------------- 1 | [entrypoints.test1] 2 | abc = foo:abc 3 | bad = this won't work 4 | -------------------------------------------------------------------------------- /tests/samples/packages4/dev.egg-info/entry_points.txt: -------------------------------------------------------------------------------- 1 | [entrypoints.test1] 2 | def = dev:def 3 | 4 | -------------------------------------------------------------------------------- /tests/test_entrypoints.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Thomas Kluyver and contributors 2 | # Distributed under the terms of the MIT license; see LICENSE file. 3 | 4 | import os.path as osp 5 | import pytest 6 | import warnings 7 | from zipfile import ZipFile 8 | 9 | import entrypoints 10 | 11 | samples_dir = osp.join(osp.dirname(__file__), 'samples') 12 | 13 | sample_path = [ 14 | osp.join(samples_dir, 'packages1'), 15 | osp.join(samples_dir, 'packages1', 'baz-0.3.egg'), 16 | osp.join(samples_dir, 'packages2'), 17 | osp.join(samples_dir, 'packages2', 'qux-0.4.egg'), 18 | ] 19 | 20 | def test_iter_files_distros(): 21 | result = entrypoints.iter_files_distros(path=sample_path) 22 | # the sample_path has 4 unique items so iter_files_distros returns 4 tuples 23 | assert len(list(result)) == 4 24 | 25 | # testing a development, egg aka installed with pip install -e . 26 | # these don't have version info in the .egg-info directory name 27 | # (eg dev-0.0.1.egg-info) 28 | path_with_dev = [osp.join(samples_dir, 'packages4')] 29 | result = entrypoints.iter_files_distros(path=path_with_dev) 30 | assert len(list(result)) == 1 31 | 32 | # duplicate dev versions should still return one result 33 | path_with_dev_duplicates = path_with_dev * 2 34 | result = entrypoints.iter_files_distros(path=path_with_dev_duplicates) 35 | assert len(list(result)) == 1 36 | 37 | def test_get_group_all(): 38 | group = entrypoints.get_group_all('entrypoints.test1', sample_path) 39 | print(group) 40 | assert len(group) == 5 41 | assert {ep.name for ep in group} == {'abc', 'rew', 'opo', 'njn'} 42 | 43 | def test_get_group_named(): 44 | group = entrypoints.get_group_named('entrypoints.test1', sample_path) 45 | print(group) 46 | assert len(group) == 4 47 | assert group['abc'].module_name == 'foo' 48 | assert group['abc'].object_name == 'abc' 49 | 50 | def test_get_single(): 51 | ep = entrypoints.get_single('entrypoints.test1', 'abc', sample_path) 52 | assert ep.module_name == 'foo' 53 | assert ep.object_name == 'abc' 54 | 55 | ep2 = entrypoints.get_single('entrypoints.test1', 'njn', sample_path) 56 | assert ep2.module_name == 'qux.extn' 57 | assert ep2.object_name == 'Njn.load' 58 | 59 | def test_dot_prefix(): 60 | ep = entrypoints.get_single('blogtool.parsers', '.rst', sample_path) 61 | assert ep.object_name == 'SomeClass.some_classmethod' 62 | assert ep.extras == ['reST'] 63 | 64 | group = entrypoints.get_group_named('blogtool.parsers', sample_path) 65 | assert set(group.keys()) == {'.rst'} 66 | 67 | def test_case_sensitive(): 68 | group = entrypoints.get_group_named('test.case_sensitive', sample_path) 69 | assert set(group.keys()) == {'Ptangle', 'ptangle'} 70 | 71 | def test_load_zip(tmpdir): 72 | whl_file = str(tmpdir / 'parmesan-1.2.whl') 73 | with ZipFile(whl_file, 'w') as whl: 74 | whl.writestr('parmesan-1.2.dist-info/entry_points.txt', 75 | b'[entrypoints.test.inzip]\na = edam:gouda') 76 | whl.writestr('gruyere-2!1b4.dev0.egg-info/entry_points.txt', 77 | b'[entrypoints.test.inzip]\nb = wensleydale:gouda') 78 | 79 | ep = entrypoints.get_single('entrypoints.test.inzip', 'a', [str(whl_file)]) 80 | assert ep.module_name == 'edam' 81 | assert ep.object_name == 'gouda' 82 | assert ep.distro.name == 'parmesan' 83 | assert ep.distro.version == '1.2' 84 | 85 | ep2 = entrypoints.get_single('entrypoints.test.inzip', 'b', [str(whl_file)]) 86 | assert ep2.module_name == 'wensleydale' 87 | assert ep2.object_name == 'gouda' 88 | assert ep2.distro.name == 'gruyere' 89 | assert ep2.distro.version == '2!1b4.dev0' 90 | 91 | def test_load(): 92 | ep = entrypoints.EntryPoint('get_ep', 'entrypoints', 'get_single', None) 93 | obj = ep.load() 94 | assert obj is entrypoints.get_single 95 | 96 | # The object part is optional (e.g. pytest plugins use just a module ref) 97 | ep = entrypoints.EntryPoint('ep_mod', 'entrypoints', None) 98 | obj = ep.load() 99 | assert obj is entrypoints 100 | 101 | def test_bad(): 102 | bad_path = [osp.join(samples_dir, 'packages3')] 103 | 104 | with warnings.catch_warnings(record=True) as w: 105 | group = entrypoints.get_group_named('entrypoints.test1', bad_path) 106 | 107 | assert 'bad' not in group 108 | assert len(w) == 1 109 | 110 | with warnings.catch_warnings(record=True) as w2, \ 111 | pytest.raises(entrypoints.NoSuchEntryPoint): 112 | ep = entrypoints.get_single('entrypoints.test1', 'bad') 113 | 114 | assert len(w) == 1 115 | 116 | def test_missing(): 117 | with pytest.raises(entrypoints.NoSuchEntryPoint) as ec: 118 | entrypoints.get_single('no.such.group', 'no_such_name', sample_path) 119 | 120 | assert ec.value.group == 'no.such.group' 121 | assert ec.value.name == 'no_such_name' 122 | 123 | def test_parse(): 124 | ep = entrypoints.EntryPoint.from_string( 125 | 'some.module:some.attr [extra1,extra2]', 'foo' 126 | ) 127 | assert ep.module_name == 'some.module' 128 | assert ep.object_name == 'some.attr' 129 | assert ep.extras == ['extra1', 'extra2'] 130 | 131 | def test_parse_bad(): 132 | with pytest.raises(entrypoints.BadEntryPoint): 133 | entrypoints.EntryPoint.from_string("this won't work", 'foo') 134 | --------------------------------------------------------------------------------