├── .bandit
├── .checks.yml
├── .editorconfig
├── .github
└── ISSUE_TEMPLATE.md
├── .gitignore
├── .travis.yml
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.md
├── depx
├── __init__.py
├── cli.py
├── depx.py
├── graph.py
├── parsing.py
└── template.html
├── docs
├── Makefile
├── conf.py
├── contributing.rst
├── history.rst
├── index.rst
├── installation.rst
├── make.bat
├── readme.rst
└── usage.rst
├── poetry.lock
├── pyproject.toml
├── setup.cfg
├── tests
├── __init__.py
├── fake_project
│ ├── another_fake_module
│ │ ├── __init__.py
│ │ └── missing_creativity.py
│ ├── fake_module
│ │ ├── __init__.py
│ │ ├── a_module.py
│ │ └── another_module.py
│ ├── random_folder
│ │ ├── cool_file.txt
│ │ └── python_file_without_py_extension.txt
│ └── setup.py
├── test_depx.py
├── test_graph.py
└── test_parsing.py
└── tox.ini
/.bandit:
--------------------------------------------------------------------------------
1 | [bandit]
2 | exclude: test
3 |
--------------------------------------------------------------------------------
/.checks.yml:
--------------------------------------------------------------------------------
1 | - bandit
2 | - flake8
3 | - pydocstyle
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 4
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 | charset = utf-8
11 | end_of_line = lf
12 |
13 | [*.bat]
14 | indent_style = tab
15 | end_of_line = crlf
16 |
17 | [LICENSE]
18 | insert_final_newline = false
19 |
20 | [Makefile]
21 | indent_style = tab
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | * Depx version:
2 | * Python version:
3 | * Operating System:
4 |
5 | ### Description
6 |
7 | Describe what you were trying to get done.
8 | Tell us what happened, what went wrong, and what you expected to happen.
9 |
10 | ### What I Did
11 |
12 | ```
13 | Paste the command(s) you ran and the output.
14 | If there was a crash, please include the traceback here.
15 | ```
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Sphinx documentation
51 | docs/_build/
52 |
53 | # PyBuilder
54 | target/
55 |
56 | # Jupyter Notebook
57 | .ipynb_checkpoints
58 |
59 | # pyenv
60 | .python-version
61 |
62 | # dotenv
63 | .env
64 |
65 | # virtualenv
66 | .venv
67 | venv/
68 | ENV/
69 |
70 | # mkdocs documentation
71 | /site
72 |
73 | # mypy
74 | .mypy_cache/
75 |
76 | # IDEs
77 | .idea/
78 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: xenial
2 | language: python
3 | python:
4 | - 3.9
5 | - 3.8
6 | - 3.7
7 | - 3.6
8 |
9 | install: pip install -U tox-travis
10 |
11 | script: tox
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2019 Thermondo GmbH
5 |
6 | This program is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU General Public License as published by
8 | the Free Software Foundation, either version 3 of the License, or
9 | (at your option) any later version.
10 |
11 | This program is distributed in the hope that it will be useful,
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | GNU General Public License for more details.
15 |
16 | You should have received a copy of the GNU General Public License
17 | along with this program. If not, see .
18 |
19 | Also add information on how to contact you by electronic and paper mail.
20 |
21 | You should also get your employer (if you work as a programmer) or school,
22 | if any, to sign a "copyright disclaimer" for the program, if necessary.
23 | For more information on this, and how to apply and follow the GNU GPL, see
24 | .
25 |
26 | The GNU General Public License does not permit incorporating your program
27 | into proprietary programs. If your program is a subroutine library, you
28 | may consider it more useful to permit linking proprietary applications with
29 | the library. If this is what you want to do, use the GNU Lesser General
30 | Public License instead of this License. But first, please read
31 | .
32 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include CONTRIBUTING.rst
2 | include HISTORY.rst
3 | include LICENSE
4 | include README.rst
5 |
6 | recursive-include tests *
7 | recursive-exclude * __pycache__
8 | recursive-exclude * *.py[co]
9 |
10 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif
11 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: clean clean-test clean-pyc clean-build docs help requirements
2 | .DEFAULT_GOAL := help
3 |
4 | define BROWSER_PYSCRIPT
5 | import os, webbrowser, sys
6 |
7 | try:
8 | from urllib import pathname2url
9 | except:
10 | from urllib.request import pathname2url
11 |
12 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
13 | endef
14 | export BROWSER_PYSCRIPT
15 |
16 | define PRINT_HELP_PYSCRIPT
17 | import re, sys
18 |
19 | for line in sys.stdin:
20 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)
21 | if match:
22 | target, help = match.groups()
23 | print("%-20s %s" % (target, help))
24 | endef
25 | export PRINT_HELP_PYSCRIPT
26 |
27 | BROWSER := python -c "$$BROWSER_PYSCRIPT"
28 |
29 | help:
30 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
31 |
32 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
33 |
34 | clean-build: ## remove build artifacts
35 | rm -fr build/
36 | rm -fr dist/
37 | rm -fr .eggs/
38 | find . -name '*.egg-info' -exec rm -fr {} +
39 | find . -name '*.egg' -exec rm -f {} +
40 |
41 | clean-pyc: ## remove Python file artifacts
42 | find . -name '*.pyc' -exec rm -f {} +
43 | find . -name '*.pyo' -exec rm -f {} +
44 | find . -name '*~' -exec rm -f {} +
45 | find . -name '__pycache__' -exec rm -fr {} +
46 |
47 | clean-test: ## remove test and coverage artifacts
48 | rm -fr .tox/
49 | rm -f .coverage
50 | rm -fr htmlcov/
51 | rm -fr .pytest_cache
52 |
53 | lint: ## check style with flake8
54 | poetry run flake8 depx tests
55 |
56 | test: ## run tests quickly with the default Python
57 | poetry run pytest
58 |
59 | test-all: ## run tests on every Python version with tox
60 | tox
61 |
62 | coverage: ## check code coverage quickly with the default Python
63 | poetry run coverage run --source depx -m pytest
64 | poetry run coverage report -m
65 | poetry run coverage html
66 | $(BROWSER) htmlcov/index.html
67 |
68 | docs: ## generate Sphinx HTML documentation, including API docs
69 | rm -f docs/depx.rst
70 | rm -f docs/modules.rst
71 | poetry run sphinx-apidoc -o docs/ depx
72 | $(MAKE) -C docs clean
73 | $(MAKE) -C docs html
74 | $(BROWSER) docs/_build/html/index.html
75 |
76 | servedocs: docs ## compile the docs watching for changes
77 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D .
78 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Depx
2 |
3 | [](https://travis-ci.org/tilboerner/depx)
4 |
5 |
6 | Examine and visualize dependencies used by Python modules.
7 |
8 | This is a simple, exploratory prototype for now. The goal is to use explicit
9 | `import` statements in a module to identify the names of its dependencies. This
10 | will not always produce a complete picture of all other code a module uses,
11 | since there is a multitude of ways to import or reference other code
12 | dynamically. Idiomatic Django, for example, uses a number of them. Those are
13 | all out of scope, however, although some might get considered at a later time.
14 |
15 | In this early stage, all functionality, structure and interfaces are subject to
16 | radical and surprising changes.
17 |
18 |
19 | ## Dev setup
20 |
21 | You're going to need [Poetry](https://python-poetry.org/) and
22 | [tox](http://tox.readthedocs.org/).
23 |
24 | After cloning the repo, set up the dev environment:
25 |
26 | $ cd depx
27 | $ poetry install --no-root
28 |
29 | Run tests:
30 |
31 | $ make test
32 |
33 | Lint:
34 |
35 | $ make lint
36 |
37 | Comprehensive test suite:
38 |
39 | $ tox
40 |
41 | Add dependencies:
42 |
43 | $ poetry add [--dev]
44 |
45 |
46 | ## Envisioned use cases
47 |
48 | The project should enable the following use cases, but might do so by handing
49 | off the right kind of data to other software. If we do different things
50 | ourselves, they should end up in different commands (or at least subcommands).
51 |
52 | * visualize dependencies of module, package or project
53 | * identify dependency cycles
54 | * help identify unused dependencies
55 |
56 |
57 | ## Existing and potential functionality
58 |
59 | Sketch of ideas. This is not meant as a plan, but to guide initial development.
60 |
61 | ### Find dependencies
62 |
63 | - [x] identify all explicit imports a module makes
64 | - [x] same for whole packages
65 | - [x] same for arbitrary directories
66 | - [x] identify local imports (those are smelly)
67 | - [ ] distinguish built-in, 3rd party and local dependencies
68 |
69 |
70 | ### Names
71 |
72 | - [x] show fully qualified names for importing module and dependency
73 | - [x] ability to show only top-level names
74 | - [x] resolve relative imports to proper names
75 | - [ ] resolve `*` imports
76 |
77 |
78 | ### Output formats
79 |
80 | depx my-awesome-project --format html
81 |
82 | The identified dependencies are the edges of a directed graph. Output formats
83 | should include several standard ways to consume such data.
84 |
85 | - [x] JSON
86 | - [x] GraphML
87 | - [x] browser-ready HTML with visualization
88 | - [x] Graphviz (`.dot`)
89 | - [ ] text with columns (to compose with Unix pipes)
90 |
--------------------------------------------------------------------------------
/depx/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """Top-level package for Depx."""
4 |
5 | __author__ = """Til Boerner"""
6 | __email__ = 'github@dxdm.org'
7 | __version__ = '0.2.0'
8 |
--------------------------------------------------------------------------------
/depx/cli.py:
--------------------------------------------------------------------------------
1 | import click
2 | from depx.graph import create_graph_from, to_json, to_html, to_graphml, to_dotfile
3 | from depx.parsing import find_imports, filter_top_level_names
4 | import sys
5 |
6 |
7 | formatters = {
8 | 'json': to_json,
9 | 'html': to_html,
10 | 'graphml': to_graphml,
11 | 'dot': to_dotfile,
12 | }
13 |
14 |
15 | @click.command()
16 | @click.argument('path')
17 | @click.option(
18 | '--format',
19 | '-f',
20 | type=click.Choice(formatters),
21 | default='json',
22 | help='Graph output format.',
23 | )
24 | @click.option(
25 | '--short-names/--no-short-names',
26 | '-s/ ',
27 | default=False,
28 | help='Use the top-level name only for all dependencies (up to first).',
29 | )
30 | def main(path, **kwargs):
31 | to_format = kwargs['format']
32 | short_names = kwargs['short_names']
33 | deps = list(find_imports(path))
34 |
35 | if short_names:
36 | deps = filter_top_level_names(deps)
37 | deps = list(deps)
38 |
39 | if deps:
40 | graph = create_graph_from(deps)
41 |
42 | export_to = formatters.get(to_format)
43 | click.echo(export_to(graph=graph, path=path, dependencies=deps))
44 |
45 | return 0
46 |
47 |
48 | if __name__ == '__main__':
49 | sys.exit(main()) # pragma: no cover
50 |
--------------------------------------------------------------------------------
/depx/depx.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """Main module."""
4 |
--------------------------------------------------------------------------------
/depx/graph.py:
--------------------------------------------------------------------------------
1 | from jinja2 import Template
2 | import json
3 | import networkx as nx
4 | from networkx.readwrite import json_graph
5 | from networkx.drawing.nx_pydot import to_pydot
6 | import io
7 | from pathlib import Path
8 |
9 |
10 | TEMPLATE_FILE = str(Path(__file__).parent.absolute() / 'template.html')
11 |
12 |
13 | def create_graph_from(dependencies):
14 | graph = nx.DiGraph()
15 |
16 | for dependency in dependencies:
17 | try:
18 | weight = graph[dependency['from_module']][dependency['to_module']]['weight']
19 | weight += 1
20 | graph[dependency['from_module']][dependency['to_module']]['weight'] = weight
21 | except KeyError:
22 | graph.add_edge(dependency['from_module'], dependency['to_module'], weight=1)
23 | return graph
24 |
25 |
26 | def to_html(**kwargs):
27 | data = json_graph.node_link_data(kwargs['graph'])
28 |
29 | with open(TEMPLATE_FILE) as file_:
30 | template = Template(file_.read())
31 | content = template.render(data=data, path=kwargs['path'])
32 | return content
33 |
34 |
35 | def to_graphml(**kwargs):
36 | in_memory_file = io.BytesIO()
37 |
38 | nx.write_graphml(kwargs['graph'], in_memory_file)
39 | return in_memory_file.getvalue().decode('utf-8')
40 |
41 |
42 | def to_dotfile(**kwargs):
43 | pydot = to_pydot(kwargs['graph'])
44 | return pydot.to_string()
45 |
46 |
47 | def to_json(**kwargs):
48 | return json.dumps(kwargs['dependencies'], indent=4)
49 |
--------------------------------------------------------------------------------
/depx/parsing.py:
--------------------------------------------------------------------------------
1 | import ast
2 | import logging
3 | import os
4 | import re
5 |
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 |
10 | def _dependency(
11 | *, from_module, to_module, category='', is_relative=False, level=0, **kwargs
12 | ):
13 | dep = {
14 | 'from_module': from_module,
15 | 'to_module': to_module, # currently, this could also be a symbol within a module
16 | 'category': category,
17 | 'is_relative': is_relative,
18 | 'level': level,
19 | }
20 | dep.update(kwargs)
21 | return dep
22 |
23 |
24 | def _goes_local(node):
25 | func_defs = (ast.FunctionDef,)
26 | try:
27 | func_defs += (ast.AsyncFunctionDef,)
28 | except AttributeError: # Py < 3.5
29 | pass
30 | return isinstance(node, func_defs)
31 |
32 |
33 | def _walk(node):
34 | from collections import deque
35 |
36 | iter_child_nodes = ast.iter_child_nodes
37 | todo = deque([(node, False)])
38 | while todo:
39 | node, is_local = todo.popleft()
40 | child_local = is_local or _goes_local(node)
41 | todo.extend((child, child_local) for child in iter_child_nodes(node))
42 | yield node, is_local
43 |
44 |
45 | def _is_package(path):
46 | init_path = os.path.join(path, '__init__.py')
47 | return os.path.isfile(init_path)
48 |
49 |
50 | def _find_base_name(path, base_name=''):
51 | parent = os.path.dirname(path)
52 | if not _is_package(parent):
53 | return base_name
54 | parent_name = os.path.basename(parent)
55 | if base_name:
56 | base_name = parent_name + '.' + base_name
57 | else:
58 | base_name = parent_name
59 | return _find_base_name(parent, base_name)
60 |
61 |
62 | def _is_module(path):
63 | if not os.path.isfile(path):
64 | return False
65 | if path.endswith('.py'):
66 | return True
67 | with open(path) as f:
68 | try:
69 | line = f.readline()
70 | except UnicodeError:
71 | return False
72 | return bool(line and re.match(r'^#!.*?python', line))
73 |
74 |
75 | def find_imports(path):
76 | if _is_module(path):
77 | yield from find_module_imports(path)
78 | else:
79 | yield from find_imports_in_directory(path)
80 |
81 |
82 | def find_imports_in_directory(directory_path):
83 | for path, subdirs, files in os.walk(directory_path):
84 | subdirs[:] = [d for d in subdirs if _is_package(os.path.join(path, d))]
85 | for filename in files:
86 | module_path = os.path.join(path, filename)
87 | if not _is_module(module_path):
88 | continue
89 | yield from find_module_imports(module_path)
90 |
91 |
92 | def find_module_imports(path, base_name=None):
93 | with open(path) as f:
94 | text = f.read()
95 | module_name = os.path.splitext(os.path.basename(path))[0]
96 | if base_name is None:
97 | base_name = _find_base_name(path)
98 | if base_name:
99 | module_name = base_name + '.' + module_name
100 | return find_imports_from_text(text, module_name)
101 |
102 |
103 | def find_imports_from_text(text, base_name):
104 | tree = ast.parse(text)
105 | for node, is_local in _walk(tree):
106 | category = 'local' if is_local else 'module'
107 | if isinstance(node, ast.Import):
108 | for alias in node.names:
109 | yield _dependency(
110 | category=category,
111 | from_module=base_name,
112 | from_name=alias.asname or alias.name,
113 | to_module=alias.name,
114 | to_name=alias.name,
115 | )
116 | elif isinstance(node, ast.ImportFrom):
117 | level = node.level
118 | module = node.module
119 | for alias in node.names:
120 | if level:
121 | to_module = (
122 | module or alias.name
123 | ) # module is None for 'from . import'
124 | to_module = _resolve_relative_name(to_module, base_name, level)
125 | else:
126 | to_module = module
127 | yield _dependency(
128 | category=category,
129 | from_module=base_name,
130 | from_name=alias.asname or alias.name,
131 | to_module=to_module,
132 | to_name=alias.name,
133 | is_relative=bool(level),
134 | level=level,
135 | )
136 |
137 |
138 | def filter_top_level_names(deps):
139 | for dep in deps:
140 | dep['from_module'] = dep['from_module'].split('.')[0]
141 | dep['to_module'] = dep['to_module'].split('.')[0]
142 | dep['from_name'] = ''
143 | dep['to_name'] = ''
144 | yield dep
145 |
146 |
147 | def _resolve_relative_name(name, base_name, level):
148 | parts = base_name and base_name.split('.')
149 | if not (parts and all(parts)) or len(parts) < level:
150 | logger.warning(
151 | 'Unable to resolve relative name %r: bad or missing basename (%r)',
152 | name,
153 | base_name,
154 | )
155 | return name
156 | resolved_base = parts[:-level]
157 | return '.'.join(resolved_base + [name])
158 |
159 |
160 | if __name__ == '__main__':
161 | from pprint import pprint
162 |
163 | for x in find_module_imports(__file__):
164 | pprint(x)
165 |
--------------------------------------------------------------------------------
/depx/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Depx
6 |
7 |
8 |
9 |
10 | Depx
11 | Path: {{ path }}
12 |
13 |
29 |
30 |
31 |
113 |
114 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = poetry run python -msphinx
7 | SPHINXPROJ = depx
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | #
4 | # depx documentation build configuration file, created by
5 | # sphinx-quickstart on Fri Jun 9 13:47:02 2017.
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 | # If extensions (or modules to document with autodoc) are in another
17 | # directory, add these directories to sys.path here. If the directory is
18 | # relative to the documentation root, use os.path.abspath to make it
19 | # absolute, like shown here.
20 | #
21 | import os
22 | import sys
23 |
24 | sys.path.insert(0, os.path.abspath('..'))
25 |
26 | import depx
27 |
28 | # -- General configuration ---------------------------------------------
29 |
30 | # If your documentation needs a minimal Sphinx version, state it here.
31 | #
32 | # needs_sphinx = '1.0'
33 |
34 | # Add any Sphinx extension module names here, as strings. They can be
35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
36 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
37 |
38 | # Add any paths that contain templates here, relative to this directory.
39 | templates_path = ['_templates']
40 |
41 | # The suffix(es) of source filenames.
42 | # You can specify multiple suffix as a list of string:
43 | #
44 | # source_suffix = ['.rst', '.md']
45 | source_suffix = '.rst'
46 |
47 | # The master toctree document.
48 | master_doc = 'index'
49 |
50 | # General information about the project.
51 | project = u'Depx'
52 | copyright = u"2019, Thermondo"
53 | author = u"Til Boerner"
54 |
55 | # The version info for the project you're documenting, acts as replacement
56 | # for |version| and |release|, also used in various other places throughout
57 | # the built documents.
58 | #
59 | # The short X.Y version.
60 | version = depx.__version__
61 | # The full version, including alpha/beta/rc tags.
62 | release = depx.__version__
63 |
64 | # The language for content autogenerated by Sphinx. Refer to documentation
65 | # for a list of supported languages.
66 | #
67 | # This is also used if you do content translation via gettext catalogs.
68 | # Usually you set "language" from the command line for these cases.
69 | language = None
70 |
71 | # List of patterns, relative to source directory, that match files and
72 | # directories to ignore when looking for source files.
73 | # This patterns also effect to html_static_path and html_extra_path
74 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
75 |
76 | # The name of the Pygments (syntax highlighting) style to use.
77 | pygments_style = 'sphinx'
78 |
79 | # If true, `todo` and `todoList` produce output, else they produce nothing.
80 | todo_include_todos = False
81 |
82 |
83 | # -- Options for HTML output -------------------------------------------
84 |
85 | # The theme to use for HTML and HTML Help pages. See the documentation for
86 | # a list of builtin themes.
87 | #
88 | html_theme = 'alabaster'
89 |
90 | # Theme options are theme-specific and customize the look and feel of a
91 | # theme further. For a list of options available for each theme, see the
92 | # documentation.
93 | #
94 | # html_theme_options = {}
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 |
102 | # -- Options for HTMLHelp output ---------------------------------------
103 |
104 | # Output file base name for HTML help builder.
105 | htmlhelp_basename = 'depxdoc'
106 |
107 |
108 | # -- Options for LaTeX output ------------------------------------------
109 |
110 | latex_elements = {
111 | # The paper size ('letterpaper' or 'a4paper').
112 | #
113 | # 'papersize': 'letterpaper',
114 | # The font size ('10pt', '11pt' or '12pt').
115 | #
116 | # 'pointsize': '10pt',
117 | # Additional stuff for the LaTeX preamble.
118 | #
119 | # 'preamble': '',
120 | # Latex figure (float) alignment
121 | #
122 | # 'figure_align': 'htbp',
123 | }
124 |
125 | # Grouping the document tree into LaTeX files. List of tuples
126 | # (source start file, target name, title, author, documentclass
127 | # [howto, manual, or own class]).
128 | latex_documents = [
129 | (master_doc, 'depx.tex', u'Depx Documentation', u'Til Boerner', 'manual'),
130 | ]
131 |
132 |
133 | # -- Options for manual page output ------------------------------------
134 |
135 | # One entry per manual page. List of tuples
136 | # (source start file, name, description, authors, manual section).
137 | man_pages = [(master_doc, 'depx', u'Depx Documentation', [author], 1)]
138 |
139 |
140 | # -- Options for Texinfo output ----------------------------------------
141 |
142 | # Grouping the document tree into Texinfo files. List of tuples
143 | # (source start file, target name, title, author,
144 | # dir menu entry, description, category)
145 | texinfo_documents = [
146 | (
147 | master_doc,
148 | 'depx',
149 | u'Depx Documentation',
150 | author,
151 | 'depx',
152 | 'One line description of project.',
153 | 'Miscellaneous',
154 | ),
155 | ]
156 |
--------------------------------------------------------------------------------
/docs/contributing.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../CONTRIBUTING.rst
2 |
--------------------------------------------------------------------------------
/docs/history.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../HISTORY.rst
2 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Welcome to Depx's documentation!
2 | ======================================
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 | :caption: Contents:
7 |
8 | readme
9 | installation
10 | usage
11 | modules
12 | contributing
13 | history
14 |
15 | Indices and tables
16 | ==================
17 | * :ref:`genindex`
18 | * :ref:`modindex`
19 | * :ref:`search`
20 |
--------------------------------------------------------------------------------
/docs/installation.rst:
--------------------------------------------------------------------------------
1 | .. highlight:: shell
2 |
3 | ============
4 | Installation
5 | ============
6 |
7 |
8 | Stable release
9 | --------------
10 |
11 | To install Depx, run this command in your terminal:
12 |
13 | .. code-block:: console
14 |
15 | $ pip install depx
16 |
17 | This is the preferred method to install Depx, as it will always install the most recent stable release.
18 |
19 | If you don't have `pip`_ installed, this `Python installation guide`_ can guide
20 | you through the process.
21 |
22 | .. _pip: https://pip.pypa.io
23 | .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/
24 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=python -msphinx
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 | set SPHINXPROJ=depx
13 |
14 | if "%1" == "" goto help
15 |
16 | %SPHINXBUILD% >NUL 2>NUL
17 | if errorlevel 9009 (
18 | echo.
19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed,
20 | echo.then set the SPHINXBUILD environment variable to point to the full
21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the
22 | echo.Sphinx directory to PATH.
23 | echo.
24 | echo.If you don't have Sphinx installed, grab it from
25 | echo.http://sphinx-doc.org/
26 | exit /b 1
27 | )
28 |
29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
30 | goto end
31 |
32 | :help
33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
34 |
35 | :end
36 | popd
37 |
--------------------------------------------------------------------------------
/docs/readme.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 |
--------------------------------------------------------------------------------
/docs/usage.rst:
--------------------------------------------------------------------------------
1 | =====
2 | Usage
3 | =====
4 |
5 | To use Depx in a project::
6 |
7 | import depx
8 |
--------------------------------------------------------------------------------
/poetry.lock:
--------------------------------------------------------------------------------
1 | [[package]]
2 | name = "alabaster"
3 | version = "0.7.12"
4 | description = "A configurable sidebar-enabled Sphinx theme"
5 | category = "dev"
6 | optional = false
7 | python-versions = "*"
8 |
9 | [[package]]
10 | name = "appdirs"
11 | version = "1.4.4"
12 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
13 | category = "dev"
14 | optional = false
15 | python-versions = "*"
16 |
17 | [[package]]
18 | name = "atomicwrites"
19 | version = "1.4.0"
20 | description = "Atomic file writes."
21 | category = "dev"
22 | optional = false
23 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
24 |
25 | [[package]]
26 | name = "attrs"
27 | version = "20.3.0"
28 | description = "Classes Without Boilerplate"
29 | category = "dev"
30 | optional = false
31 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
32 |
33 | [package.extras]
34 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"]
35 | docs = ["furo", "sphinx", "zope.interface"]
36 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
37 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"]
38 |
39 | [[package]]
40 | name = "babel"
41 | version = "2.8.0"
42 | description = "Internationalization utilities"
43 | category = "dev"
44 | optional = false
45 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
46 |
47 | [package.dependencies]
48 | pytz = ">=2015.7"
49 |
50 | [[package]]
51 | name = "black"
52 | version = "20.8b1"
53 | description = "The uncompromising code formatter."
54 | category = "dev"
55 | optional = false
56 | python-versions = ">=3.6"
57 |
58 | [package.dependencies]
59 | appdirs = "*"
60 | click = ">=7.1.2"
61 | dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""}
62 | mypy-extensions = ">=0.4.3"
63 | pathspec = ">=0.6,<1"
64 | regex = ">=2020.1.8"
65 | toml = ">=0.10.1"
66 | typed-ast = ">=1.4.0"
67 | typing-extensions = ">=3.7.4"
68 |
69 | [package.extras]
70 | colorama = ["colorama (>=0.4.3)"]
71 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
72 |
73 | [[package]]
74 | name = "certifi"
75 | version = "2020.11.8"
76 | description = "Python package for providing Mozilla's CA Bundle."
77 | category = "dev"
78 | optional = false
79 | python-versions = "*"
80 |
81 | [[package]]
82 | name = "chardet"
83 | version = "3.0.4"
84 | description = "Universal encoding detector for Python 2 and 3"
85 | category = "dev"
86 | optional = false
87 | python-versions = "*"
88 |
89 | [[package]]
90 | name = "click"
91 | version = "7.1.2"
92 | description = "Composable command line interface toolkit"
93 | category = "main"
94 | optional = false
95 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
96 |
97 | [[package]]
98 | name = "colorama"
99 | version = "0.4.4"
100 | description = "Cross-platform colored terminal text."
101 | category = "dev"
102 | optional = false
103 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
104 |
105 | [[package]]
106 | name = "coverage"
107 | version = "5.3"
108 | description = "Code coverage measurement for Python"
109 | category = "dev"
110 | optional = false
111 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
112 |
113 | [package.extras]
114 | toml = ["toml"]
115 |
116 | [[package]]
117 | name = "dataclasses"
118 | version = "0.7"
119 | description = "A backport of the dataclasses module for Python 3.6"
120 | category = "dev"
121 | optional = false
122 | python-versions = ">=3.6, <3.7"
123 |
124 | [[package]]
125 | name = "decorator"
126 | version = "4.4.2"
127 | description = "Decorators for Humans"
128 | category = "main"
129 | optional = false
130 | python-versions = ">=2.6, !=3.0.*, !=3.1.*"
131 |
132 | [[package]]
133 | name = "docutils"
134 | version = "0.16"
135 | description = "Docutils -- Python Documentation Utilities"
136 | category = "dev"
137 | optional = false
138 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
139 |
140 | [[package]]
141 | name = "flake8"
142 | version = "3.8.4"
143 | description = "the modular source code checker: pep8 pyflakes and co"
144 | category = "dev"
145 | optional = false
146 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
147 |
148 | [package.dependencies]
149 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
150 | mccabe = ">=0.6.0,<0.7.0"
151 | pycodestyle = ">=2.6.0a1,<2.7.0"
152 | pyflakes = ">=2.2.0,<2.3.0"
153 |
154 | [[package]]
155 | name = "idna"
156 | version = "2.10"
157 | description = "Internationalized Domain Names in Applications (IDNA)"
158 | category = "dev"
159 | optional = false
160 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
161 |
162 | [[package]]
163 | name = "imagesize"
164 | version = "1.2.0"
165 | description = "Getting image size from png/jpeg/jpeg2000/gif file"
166 | category = "dev"
167 | optional = false
168 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
169 |
170 | [[package]]
171 | name = "importlib-metadata"
172 | version = "2.0.0"
173 | description = "Read metadata from Python packages"
174 | category = "dev"
175 | optional = false
176 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
177 |
178 | [package.dependencies]
179 | zipp = ">=0.5"
180 |
181 | [package.extras]
182 | docs = ["sphinx", "rst.linker"]
183 | testing = ["packaging", "pep517", "importlib-resources (>=1.3)"]
184 |
185 | [[package]]
186 | name = "iniconfig"
187 | version = "1.1.1"
188 | description = "iniconfig: brain-dead simple config-ini parsing"
189 | category = "dev"
190 | optional = false
191 | python-versions = "*"
192 |
193 | [[package]]
194 | name = "jinja2"
195 | version = "2.11.2"
196 | description = "A very fast and expressive template engine."
197 | category = "main"
198 | optional = false
199 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
200 |
201 | [package.dependencies]
202 | MarkupSafe = ">=0.23"
203 |
204 | [package.extras]
205 | i18n = ["Babel (>=0.8)"]
206 |
207 | [[package]]
208 | name = "markupsafe"
209 | version = "1.1.1"
210 | description = "Safely add untrusted strings to HTML/XML markup."
211 | category = "main"
212 | optional = false
213 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
214 |
215 | [[package]]
216 | name = "mccabe"
217 | version = "0.6.1"
218 | description = "McCabe checker, plugin for flake8"
219 | category = "dev"
220 | optional = false
221 | python-versions = "*"
222 |
223 | [[package]]
224 | name = "mypy-extensions"
225 | version = "0.4.3"
226 | description = "Experimental type system extensions for programs checked with the mypy typechecker."
227 | category = "dev"
228 | optional = false
229 | python-versions = "*"
230 |
231 | [[package]]
232 | name = "networkx"
233 | version = "2.5"
234 | description = "Python package for creating and manipulating graphs and networks"
235 | category = "main"
236 | optional = false
237 | python-versions = ">=3.6"
238 |
239 | [package.dependencies]
240 | decorator = ">=4.3.0"
241 |
242 | [package.extras]
243 | all = ["numpy", "scipy", "pandas", "matplotlib", "pygraphviz", "pydot", "pyyaml", "lxml", "pytest"]
244 | gdal = ["gdal"]
245 | lxml = ["lxml"]
246 | matplotlib = ["matplotlib"]
247 | numpy = ["numpy"]
248 | pandas = ["pandas"]
249 | pydot = ["pydot"]
250 | pygraphviz = ["pygraphviz"]
251 | pytest = ["pytest"]
252 | pyyaml = ["pyyaml"]
253 | scipy = ["scipy"]
254 |
255 | [[package]]
256 | name = "packaging"
257 | version = "20.4"
258 | description = "Core utilities for Python packages"
259 | category = "dev"
260 | optional = false
261 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
262 |
263 | [package.dependencies]
264 | pyparsing = ">=2.0.2"
265 | six = "*"
266 |
267 | [[package]]
268 | name = "pathspec"
269 | version = "0.8.1"
270 | description = "Utility library for gitignore style pattern matching of file paths."
271 | category = "dev"
272 | optional = false
273 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
274 |
275 | [[package]]
276 | name = "pluggy"
277 | version = "0.13.1"
278 | description = "plugin and hook calling mechanisms for python"
279 | category = "dev"
280 | optional = false
281 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
282 |
283 | [package.dependencies]
284 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
285 |
286 | [package.extras]
287 | dev = ["pre-commit", "tox"]
288 |
289 | [[package]]
290 | name = "py"
291 | version = "1.9.0"
292 | description = "library with cross-python path, ini-parsing, io, code, log facilities"
293 | category = "dev"
294 | optional = false
295 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
296 |
297 | [[package]]
298 | name = "pycodestyle"
299 | version = "2.6.0"
300 | description = "Python style guide checker"
301 | category = "dev"
302 | optional = false
303 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
304 |
305 | [[package]]
306 | name = "pydot"
307 | version = "1.4.1"
308 | description = "Python interface to Graphviz's Dot"
309 | category = "main"
310 | optional = false
311 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
312 |
313 | [package.dependencies]
314 | pyparsing = ">=2.1.4"
315 |
316 | [[package]]
317 | name = "pyflakes"
318 | version = "2.2.0"
319 | description = "passive checker of Python programs"
320 | category = "dev"
321 | optional = false
322 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
323 |
324 | [[package]]
325 | name = "pygments"
326 | version = "2.7.2"
327 | description = "Pygments is a syntax highlighting package written in Python."
328 | category = "dev"
329 | optional = false
330 | python-versions = ">=3.5"
331 |
332 | [[package]]
333 | name = "pyparsing"
334 | version = "2.4.7"
335 | description = "Python parsing module"
336 | category = "main"
337 | optional = false
338 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
339 |
340 | [[package]]
341 | name = "pytest"
342 | version = "6.1.2"
343 | description = "pytest: simple powerful testing with Python"
344 | category = "dev"
345 | optional = false
346 | python-versions = ">=3.5"
347 |
348 | [package.dependencies]
349 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
350 | attrs = ">=17.4.0"
351 | colorama = {version = "*", markers = "sys_platform == \"win32\""}
352 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
353 | iniconfig = "*"
354 | packaging = "*"
355 | pluggy = ">=0.12,<1.0"
356 | py = ">=1.8.2"
357 | toml = "*"
358 |
359 | [package.extras]
360 | checkqa_mypy = ["mypy (==0.780)"]
361 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
362 |
363 | [[package]]
364 | name = "pytz"
365 | version = "2020.4"
366 | description = "World timezone definitions, modern and historical"
367 | category = "dev"
368 | optional = false
369 | python-versions = "*"
370 |
371 | [[package]]
372 | name = "regex"
373 | version = "2020.10.28"
374 | description = "Alternative regular expression module, to replace re."
375 | category = "dev"
376 | optional = false
377 | python-versions = "*"
378 |
379 | [[package]]
380 | name = "requests"
381 | version = "2.24.0"
382 | description = "Python HTTP for Humans."
383 | category = "dev"
384 | optional = false
385 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
386 |
387 | [package.dependencies]
388 | certifi = ">=2017.4.17"
389 | chardet = ">=3.0.2,<4"
390 | idna = ">=2.5,<3"
391 | urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
392 |
393 | [package.extras]
394 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
395 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
396 |
397 | [[package]]
398 | name = "six"
399 | version = "1.15.0"
400 | description = "Python 2 and 3 compatibility utilities"
401 | category = "dev"
402 | optional = false
403 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
404 |
405 | [[package]]
406 | name = "snowballstemmer"
407 | version = "2.0.0"
408 | description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms."
409 | category = "dev"
410 | optional = false
411 | python-versions = "*"
412 |
413 | [[package]]
414 | name = "sphinx"
415 | version = "3.3.0"
416 | description = "Python documentation generator"
417 | category = "dev"
418 | optional = false
419 | python-versions = ">=3.5"
420 |
421 | [package.dependencies]
422 | alabaster = ">=0.7,<0.8"
423 | babel = ">=1.3"
424 | colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""}
425 | docutils = ">=0.12"
426 | imagesize = "*"
427 | Jinja2 = ">=2.3"
428 | packaging = "*"
429 | Pygments = ">=2.0"
430 | requests = ">=2.5.0"
431 | snowballstemmer = ">=1.1"
432 | sphinxcontrib-applehelp = "*"
433 | sphinxcontrib-devhelp = "*"
434 | sphinxcontrib-htmlhelp = "*"
435 | sphinxcontrib-jsmath = "*"
436 | sphinxcontrib-qthelp = "*"
437 | sphinxcontrib-serializinghtml = "*"
438 |
439 | [package.extras]
440 | docs = ["sphinxcontrib-websupport"]
441 | lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.790)", "docutils-stubs"]
442 | test = ["pytest", "pytest-cov", "html5lib", "typed-ast", "cython"]
443 |
444 | [[package]]
445 | name = "sphinxcontrib-applehelp"
446 | version = "1.0.2"
447 | description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
448 | category = "dev"
449 | optional = false
450 | python-versions = ">=3.5"
451 |
452 | [package.extras]
453 | lint = ["flake8", "mypy", "docutils-stubs"]
454 | test = ["pytest"]
455 |
456 | [[package]]
457 | name = "sphinxcontrib-devhelp"
458 | version = "1.0.2"
459 | description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
460 | category = "dev"
461 | optional = false
462 | python-versions = ">=3.5"
463 |
464 | [package.extras]
465 | lint = ["flake8", "mypy", "docutils-stubs"]
466 | test = ["pytest"]
467 |
468 | [[package]]
469 | name = "sphinxcontrib-htmlhelp"
470 | version = "1.0.3"
471 | description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
472 | category = "dev"
473 | optional = false
474 | python-versions = ">=3.5"
475 |
476 | [package.extras]
477 | lint = ["flake8", "mypy", "docutils-stubs"]
478 | test = ["pytest", "html5lib"]
479 |
480 | [[package]]
481 | name = "sphinxcontrib-jsmath"
482 | version = "1.0.1"
483 | description = "A sphinx extension which renders display math in HTML via JavaScript"
484 | category = "dev"
485 | optional = false
486 | python-versions = ">=3.5"
487 |
488 | [package.extras]
489 | test = ["pytest", "flake8", "mypy"]
490 |
491 | [[package]]
492 | name = "sphinxcontrib-qthelp"
493 | version = "1.0.3"
494 | description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
495 | category = "dev"
496 | optional = false
497 | python-versions = ">=3.5"
498 |
499 | [package.extras]
500 | lint = ["flake8", "mypy", "docutils-stubs"]
501 | test = ["pytest"]
502 |
503 | [[package]]
504 | name = "sphinxcontrib-serializinghtml"
505 | version = "1.1.4"
506 | description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
507 | category = "dev"
508 | optional = false
509 | python-versions = ">=3.5"
510 |
511 | [package.extras]
512 | lint = ["flake8", "mypy", "docutils-stubs"]
513 | test = ["pytest"]
514 |
515 | [[package]]
516 | name = "toml"
517 | version = "0.10.2"
518 | description = "Python Library for Tom's Obvious, Minimal Language"
519 | category = "dev"
520 | optional = false
521 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
522 |
523 | [[package]]
524 | name = "typed-ast"
525 | version = "1.4.1"
526 | description = "a fork of Python 2 and 3 ast modules with type comment support"
527 | category = "dev"
528 | optional = false
529 | python-versions = "*"
530 |
531 | [[package]]
532 | name = "typing-extensions"
533 | version = "3.7.4.3"
534 | description = "Backported and Experimental Type Hints for Python 3.5+"
535 | category = "dev"
536 | optional = false
537 | python-versions = "*"
538 |
539 | [[package]]
540 | name = "urllib3"
541 | version = "1.25.11"
542 | description = "HTTP library with thread-safe connection pooling, file post, and more."
543 | category = "dev"
544 | optional = false
545 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
546 |
547 | [package.extras]
548 | brotli = ["brotlipy (>=0.6.0)"]
549 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
550 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
551 |
552 | [[package]]
553 | name = "zipp"
554 | version = "3.4.0"
555 | description = "Backport of pathlib-compatible object wrapper for zip files"
556 | category = "dev"
557 | optional = false
558 | python-versions = ">=3.6"
559 |
560 | [package.extras]
561 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
562 | testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
563 |
564 | [metadata]
565 | lock-version = "1.1"
566 | python-versions = "^3.6"
567 | content-hash = "60ef7abbefbd7da702a5539700e590fe7c54dbde59da7d81c0af8df5e7b1de83"
568 |
569 | [metadata.files]
570 | alabaster = [
571 | {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"},
572 | {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
573 | ]
574 | appdirs = [
575 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
576 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
577 | ]
578 | atomicwrites = [
579 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
580 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
581 | ]
582 | attrs = [
583 | {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"},
584 | {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
585 | ]
586 | babel = [
587 | {file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"},
588 | {file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"},
589 | ]
590 | black = [
591 | {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
592 | ]
593 | certifi = [
594 | {file = "certifi-2020.11.8-py2.py3-none-any.whl", hash = "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd"},
595 | {file = "certifi-2020.11.8.tar.gz", hash = "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"},
596 | ]
597 | chardet = [
598 | {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
599 | {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
600 | ]
601 | click = [
602 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
603 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
604 | ]
605 | colorama = [
606 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
607 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
608 | ]
609 | coverage = [
610 | {file = "coverage-5.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270"},
611 | {file = "coverage-5.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4"},
612 | {file = "coverage-5.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9"},
613 | {file = "coverage-5.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729"},
614 | {file = "coverage-5.3-cp27-cp27m-win32.whl", hash = "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d"},
615 | {file = "coverage-5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418"},
616 | {file = "coverage-5.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9"},
617 | {file = "coverage-5.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5"},
618 | {file = "coverage-5.3-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822"},
619 | {file = "coverage-5.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097"},
620 | {file = "coverage-5.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9"},
621 | {file = "coverage-5.3-cp35-cp35m-win32.whl", hash = "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636"},
622 | {file = "coverage-5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f"},
623 | {file = "coverage-5.3-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237"},
624 | {file = "coverage-5.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54"},
625 | {file = "coverage-5.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7"},
626 | {file = "coverage-5.3-cp36-cp36m-win32.whl", hash = "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a"},
627 | {file = "coverage-5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d"},
628 | {file = "coverage-5.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"},
629 | {file = "coverage-5.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f"},
630 | {file = "coverage-5.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c"},
631 | {file = "coverage-5.3-cp37-cp37m-win32.whl", hash = "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751"},
632 | {file = "coverage-5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709"},
633 | {file = "coverage-5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516"},
634 | {file = "coverage-5.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f"},
635 | {file = "coverage-5.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259"},
636 | {file = "coverage-5.3-cp38-cp38-win32.whl", hash = "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82"},
637 | {file = "coverage-5.3-cp38-cp38-win_amd64.whl", hash = "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221"},
638 | {file = "coverage-5.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978"},
639 | {file = "coverage-5.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21"},
640 | {file = "coverage-5.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24"},
641 | {file = "coverage-5.3-cp39-cp39-win32.whl", hash = "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7"},
642 | {file = "coverage-5.3-cp39-cp39-win_amd64.whl", hash = "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7"},
643 | {file = "coverage-5.3.tar.gz", hash = "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0"},
644 | ]
645 | dataclasses = [
646 | {file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"},
647 | {file = "dataclasses-0.7.tar.gz", hash = "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"},
648 | ]
649 | decorator = [
650 | {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"},
651 | {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"},
652 | ]
653 | docutils = [
654 | {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"},
655 | {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"},
656 | ]
657 | flake8 = [
658 | {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"},
659 | {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"},
660 | ]
661 | idna = [
662 | {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
663 | {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
664 | ]
665 | imagesize = [
666 | {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"},
667 | {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"},
668 | ]
669 | importlib-metadata = [
670 | {file = "importlib_metadata-2.0.0-py2.py3-none-any.whl", hash = "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3"},
671 | {file = "importlib_metadata-2.0.0.tar.gz", hash = "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da"},
672 | ]
673 | iniconfig = [
674 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
675 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
676 | ]
677 | jinja2 = [
678 | {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
679 | {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
680 | ]
681 | markupsafe = [
682 | {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
683 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
684 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"},
685 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"},
686 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"},
687 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"},
688 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"},
689 | {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"},
690 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"},
691 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"},
692 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"},
693 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"},
694 | {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"},
695 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"},
696 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"},
697 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"},
698 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"},
699 | {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"},
700 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"},
701 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"},
702 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"},
703 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"},
704 | {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"},
705 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"},
706 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"},
707 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"},
708 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"},
709 | {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"},
710 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"},
711 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"},
712 | {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"},
713 | {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
714 | {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
715 | ]
716 | mccabe = [
717 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
718 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
719 | ]
720 | mypy-extensions = [
721 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
722 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
723 | ]
724 | networkx = [
725 | {file = "networkx-2.5-py3-none-any.whl", hash = "sha256:8c5812e9f798d37c50570d15c4a69d5710a18d77bafc903ee9c5fba7454c616c"},
726 | {file = "networkx-2.5.tar.gz", hash = "sha256:7978955423fbc9639c10498878be59caf99b44dc304c2286162fd24b458c1602"},
727 | ]
728 | packaging = [
729 | {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"},
730 | {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"},
731 | ]
732 | pathspec = [
733 | {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
734 | {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
735 | ]
736 | pluggy = [
737 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
738 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
739 | ]
740 | py = [
741 | {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
742 | {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
743 | ]
744 | pycodestyle = [
745 | {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},
746 | {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"},
747 | ]
748 | pydot = [
749 | {file = "pydot-1.4.1-py2.py3-none-any.whl", hash = "sha256:67be714300c78fda5fd52f79ec994039e3f76f074948c67b5ff539b433ad354f"},
750 | {file = "pydot-1.4.1.tar.gz", hash = "sha256:d49c9d4dd1913beec2a997f831543c8cbd53e535b1a739e921642fe416235f01"},
751 | ]
752 | pyflakes = [
753 | {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"},
754 | {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
755 | ]
756 | pygments = [
757 | {file = "Pygments-2.7.2-py3-none-any.whl", hash = "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773"},
758 | {file = "Pygments-2.7.2.tar.gz", hash = "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0"},
759 | ]
760 | pyparsing = [
761 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
762 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
763 | ]
764 | pytest = [
765 | {file = "pytest-6.1.2-py3-none-any.whl", hash = "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe"},
766 | {file = "pytest-6.1.2.tar.gz", hash = "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"},
767 | ]
768 | pytz = [
769 | {file = "pytz-2020.4-py2.py3-none-any.whl", hash = "sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd"},
770 | {file = "pytz-2020.4.tar.gz", hash = "sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268"},
771 | ]
772 | regex = [
773 | {file = "regex-2020.10.28-cp27-cp27m-win32.whl", hash = "sha256:4b5a9bcb56cc146c3932c648603b24514447eafa6ce9295234767bf92f69b504"},
774 | {file = "regex-2020.10.28-cp27-cp27m-win_amd64.whl", hash = "sha256:c13d311a4c4a8d671f5860317eb5f09591fbe8259676b86a85769423b544451e"},
775 | {file = "regex-2020.10.28-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c454ad88e56e80e44f824ef8366bb7e4c3def12999151fd5c0ea76a18fe9aa3e"},
776 | {file = "regex-2020.10.28-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c8a2b7ccff330ae4c460aff36626f911f918555660cc28163417cb84ffb25789"},
777 | {file = "regex-2020.10.28-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4afa350f162551cf402bfa3cd8302165c8e03e689c897d185f16a167328cc6dd"},
778 | {file = "regex-2020.10.28-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b88fa3b8a3469f22b4f13d045d9bd3eda797aa4e406fde0a2644bc92bbdd4bdd"},
779 | {file = "regex-2020.10.28-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f43109822df2d3faac7aad79613f5f02e4eab0fc8ad7932d2e70e2a83bd49c26"},
780 | {file = "regex-2020.10.28-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:de7fd57765398d141949946c84f3590a68cf5887dac3fc52388df0639b01eda4"},
781 | {file = "regex-2020.10.28-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:9b6305295b6591e45f069d3553c54d50cc47629eb5c218aac99e0f7fafbf90a1"},
782 | {file = "regex-2020.10.28-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:bd904c0dec29bbd0769887a816657491721d5f545c29e30fd9d7a1a275dc80ab"},
783 | {file = "regex-2020.10.28-cp36-cp36m-win32.whl", hash = "sha256:8092a5a06ad9a7a247f2a76ace121183dc4e1a84c259cf9c2ce3bbb69fac3582"},
784 | {file = "regex-2020.10.28-cp36-cp36m-win_amd64.whl", hash = "sha256:49461446b783945597c4076aea3f49aee4b4ce922bd241e4fcf62a3e7c61794c"},
785 | {file = "regex-2020.10.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:297116e79074ec2a2f885d22db00ce6e88b15f75162c5e8b38f66ea734e73c64"},
786 | {file = "regex-2020.10.28-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8ca9dca965bd86ea3631b975d63b0693566d3cc347e55786d5514988b6f5b84c"},
787 | {file = "regex-2020.10.28-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ea37320877d56a7f0a1e6a625d892cf963aa7f570013499f5b8d5ab8402b5625"},
788 | {file = "regex-2020.10.28-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:3a5f08039eee9ea195a89e180c5762bfb55258bfb9abb61a20d3abee3b37fd12"},
789 | {file = "regex-2020.10.28-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:cb905f3d2e290a8b8f1579d3984f2cfa7c3a29cc7cba608540ceeed18513f520"},
790 | {file = "regex-2020.10.28-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:96f99219dddb33e235a37283306834700b63170d7bb2a1ee17e41c6d589c8eb9"},
791 | {file = "regex-2020.10.28-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:227a8d2e5282c2b8346e7f68aa759e0331a0b4a890b55a5cfbb28bd0261b84c0"},
792 | {file = "regex-2020.10.28-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:2564def9ce0710d510b1fc7e5178ce2d20f75571f788b5197b3c8134c366f50c"},
793 | {file = "regex-2020.10.28-cp37-cp37m-win32.whl", hash = "sha256:a62162be05edf64f819925ea88d09d18b09bebf20971b363ce0c24e8b4aa14c0"},
794 | {file = "regex-2020.10.28-cp37-cp37m-win_amd64.whl", hash = "sha256:03855ee22980c3e4863dc84c42d6d2901133362db5daf4c36b710dd895d78f0a"},
795 | {file = "regex-2020.10.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf4f896c42c63d1f22039ad57de2644c72587756c0cfb3cc3b7530cfe228277f"},
796 | {file = "regex-2020.10.28-cp38-cp38-manylinux1_i686.whl", hash = "sha256:625116aca6c4b57c56ea3d70369cacc4d62fead4930f8329d242e4fe7a58ce4b"},
797 | {file = "regex-2020.10.28-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2dc522e25e57e88b4980d2bdd334825dbf6fa55f28a922fc3bfa60cc09e5ef53"},
798 | {file = "regex-2020.10.28-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:119e0355dbdd4cf593b17f2fc5dbd4aec2b8899d0057e4957ba92f941f704bf5"},
799 | {file = "regex-2020.10.28-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:cfcf28ed4ce9ced47b9b9670a4f0d3d3c0e4d4779ad4dadb1ad468b097f808aa"},
800 | {file = "regex-2020.10.28-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:b45bab9f224de276b7bc916f6306b86283f6aa8afe7ed4133423efb42015a898"},
801 | {file = "regex-2020.10.28-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:52e83a5f28acd621ba8e71c2b816f6541af7144b69cc5859d17da76c436a5427"},
802 | {file = "regex-2020.10.28-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:aacc8623ffe7999a97935eeabbd24b1ae701d08ea8f874a6ff050e93c3e658cf"},
803 | {file = "regex-2020.10.28-cp38-cp38-win32.whl", hash = "sha256:06b52815d4ad38d6524666e0d50fe9173533c9cc145a5779b89733284e6f688f"},
804 | {file = "regex-2020.10.28-cp38-cp38-win_amd64.whl", hash = "sha256:c3466a84fce42c2016113101018a9981804097bacbab029c2d5b4fcb224b89de"},
805 | {file = "regex-2020.10.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:127a9e0c0d91af572fbb9e56d00a504dbd4c65e574ddda3d45b55722462210de"},
806 | {file = "regex-2020.10.28-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c2c6c56ee97485a127555c9595c069201b5161de9d05495fbe2132b5ac104786"},
807 | {file = "regex-2020.10.28-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1ec66700a10e3c75f1f92cbde36cca0d3aaee4c73dfa26699495a3a30b09093c"},
808 | {file = "regex-2020.10.28-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:11116d424734fe356d8777f89d625f0df783251ada95d6261b4c36ad27a394bb"},
809 | {file = "regex-2020.10.28-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f1fce1e4929157b2afeb4bb7069204d4370bab9f4fc03ca1fbec8bd601f8c87d"},
810 | {file = "regex-2020.10.28-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:3dfca201fa6b326239e1bccb00b915e058707028809b8ecc0cf6819ad233a740"},
811 | {file = "regex-2020.10.28-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:b8a686a6c98872007aa41fdbb2e86dc03b287d951ff4a7f1da77fb7f14113e4d"},
812 | {file = "regex-2020.10.28-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c32c91a0f1ac779cbd73e62430de3d3502bbc45ffe5bb6c376015acfa848144b"},
813 | {file = "regex-2020.10.28-cp39-cp39-win32.whl", hash = "sha256:832339223b9ce56b7b15168e691ae654d345ac1635eeb367ade9ecfe0e66bee0"},
814 | {file = "regex-2020.10.28-cp39-cp39-win_amd64.whl", hash = "sha256:654c1635f2313d0843028487db2191530bca45af61ca85d0b16555c399625b0e"},
815 | {file = "regex-2020.10.28.tar.gz", hash = "sha256:dd3e6547ecf842a29cf25123fbf8d2461c53c8d37aa20d87ecee130c89b7079b"},
816 | ]
817 | requests = [
818 | {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"},
819 | {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"},
820 | ]
821 | six = [
822 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
823 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
824 | ]
825 | snowballstemmer = [
826 | {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"},
827 | {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"},
828 | ]
829 | sphinx = [
830 | {file = "Sphinx-3.3.0-py3-none-any.whl", hash = "sha256:3abdb2c57a65afaaa4f8573cbabd5465078eb6fd282c1e4f87f006875a7ec0c7"},
831 | {file = "Sphinx-3.3.0.tar.gz", hash = "sha256:1c21e7c5481a31b531e6cbf59c3292852ccde175b504b00ce2ff0b8f4adc3649"},
832 | ]
833 | sphinxcontrib-applehelp = [
834 | {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
835 | {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"},
836 | ]
837 | sphinxcontrib-devhelp = [
838 | {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"},
839 | {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"},
840 | ]
841 | sphinxcontrib-htmlhelp = [
842 | {file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"},
843 | {file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"},
844 | ]
845 | sphinxcontrib-jsmath = [
846 | {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
847 | {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
848 | ]
849 | sphinxcontrib-qthelp = [
850 | {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"},
851 | {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"},
852 | ]
853 | sphinxcontrib-serializinghtml = [
854 | {file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"},
855 | {file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"},
856 | ]
857 | toml = [
858 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
859 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
860 | ]
861 | typed-ast = [
862 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
863 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"},
864 | {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"},
865 | {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"},
866 | {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"},
867 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"},
868 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"},
869 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f"},
870 | {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"},
871 | {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"},
872 | {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"},
873 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"},
874 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"},
875 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298"},
876 | {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"},
877 | {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"},
878 | {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"},
879 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"},
880 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"},
881 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d"},
882 | {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"},
883 | {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"},
884 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"},
885 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c"},
886 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072"},
887 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91"},
888 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d"},
889 | {file = "typed_ast-1.4.1-cp39-cp39-win32.whl", hash = "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395"},
890 | {file = "typed_ast-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c"},
891 | {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
892 | ]
893 | typing-extensions = [
894 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
895 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
896 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
897 | ]
898 | urllib3 = [
899 | {file = "urllib3-1.25.11-py2.py3-none-any.whl", hash = "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"},
900 | {file = "urllib3-1.25.11.tar.gz", hash = "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2"},
901 | ]
902 | zipp = [
903 | {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"},
904 | {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"},
905 | ]
906 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "depx"
3 | version = "0.2.0"
4 | description = "Examine and visualize dependencies used by Python modules."
5 | authors = [
6 | "Ana Paula Gomes ",
7 | "Til Boerner ",
8 | "Thermondo "
9 | ]
10 | license = "GPL-3.0-or-later"
11 | readme = "README.md"
12 | homepage = "https://github.com/tilboerner/depx"
13 | repository = "https://github.com/tilboerner/depx.git"
14 | classifiers = [
15 | "Development Status :: 2 - Pre-Alpha",
16 | "Intended Audience :: Developers",
17 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
18 | ]
19 |
20 | [tool.poetry.scripts]
21 | depx = "depx.cli:main"
22 |
23 | [tool.poetry.urls]
24 | "Bug Tracker" = "https://github.com/tilboerner/depx/issues"
25 |
26 | [tool.poetry.dependencies]
27 | python = "^3.6"
28 | Click = "^7.1.2"
29 | jinja2 = "^2.11"
30 | networkx = "^2.3"
31 | pydot = "^1.4"
32 |
33 | [tool.poetry.dev-dependencies]
34 | pytest = "^6.1"
35 | flake8 = "^3.8.4"
36 | coverage = "^5.3"
37 | Sphinx = "^3.3"
38 | black = "^20.8b1"
39 |
40 | [build-system]
41 | requires = ["poetry>=1.0"]
42 | build-backend = "poetry.masonry.api"
43 |
44 | [tool.black]
45 | line-length = 88
46 | skip-string-normalization = true
47 | target-version = ['py36']
48 | include = '\.pyi?$'
49 | exclude = '''
50 |
51 | (
52 | /(
53 | \.eggs # exclude a few common directories in the
54 | | \.git # root of the project
55 | | \.hg
56 | | \.mypy_cache
57 | | \.tox
58 | | \.venv
59 | | _build
60 | | buck-out
61 | | build
62 | | dist
63 | )/
64 | )
65 | '''
66 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | exclude = docs
3 | max-line-length = 99
4 |
5 | [pydocstyle]
6 | add-ignore = D1
7 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tilboerner/depx/0f44d747844b0f370bf58544741d86251da5d8d6/tests/__init__.py
--------------------------------------------------------------------------------
/tests/fake_project/another_fake_module/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tilboerner/depx/0f44d747844b0f370bf58544741d86251da5d8d6/tests/fake_project/another_fake_module/__init__.py
--------------------------------------------------------------------------------
/tests/fake_project/another_fake_module/missing_creativity.py:
--------------------------------------------------------------------------------
1 | from fake_module.another_module import oh_nice
2 | import random
3 |
4 |
5 | def pick_a_number():
6 | print(oh_nice())
7 | return random.randint(1, 10)
8 |
--------------------------------------------------------------------------------
/tests/fake_project/fake_module/__init__.py:
--------------------------------------------------------------------------------
1 | import fake_module.a_module
2 | from fake_module import another_module
3 |
4 |
5 | fake_module.a_module.oh_nice()
6 | another_module.oh_nice()
7 |
--------------------------------------------------------------------------------
/tests/fake_project/fake_module/a_module.py:
--------------------------------------------------------------------------------
1 | from .another_module import oh_nice
2 |
3 |
4 | oh_nice()
5 |
--------------------------------------------------------------------------------
/tests/fake_project/fake_module/another_module.py:
--------------------------------------------------------------------------------
1 | def oh_nice():
2 | return 'oh, nice!'
3 |
--------------------------------------------------------------------------------
/tests/fake_project/random_folder/cool_file.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tilboerner/depx/0f44d747844b0f370bf58544741d86251da5d8d6/tests/fake_project/random_folder/cool_file.txt
--------------------------------------------------------------------------------
/tests/fake_project/random_folder/python_file_without_py_extension.txt:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
--------------------------------------------------------------------------------
/tests/fake_project/setup.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tilboerner/depx/0f44d747844b0f370bf58544741d86251da5d8d6/tests/fake_project/setup.py
--------------------------------------------------------------------------------
/tests/test_depx.py:
--------------------------------------------------------------------------------
1 | from click.testing import CliRunner
2 | import pytest
3 |
4 | import depx
5 | from depx import cli
6 |
7 |
8 | fake_module = 'tests/fake_project/fake_module'
9 |
10 |
11 | def test_package_version_matches_project():
12 | import pkg_resources
13 |
14 | assert depx.__version__ == pkg_resources.get_distribution('depx').version
15 |
16 |
17 | def test_command_line_interface():
18 | runner = CliRunner()
19 | run_result = runner.invoke(cli.main, [fake_module])
20 |
21 | assert run_result.exit_code == 0
22 |
23 |
24 | @pytest.mark.parametrize('format', ['json', 'html', 'graphml', 'dot'])
25 | def test_export_to(format):
26 | runner = CliRunner()
27 | run_result = runner.invoke(cli.main, [fake_module, '--format', format])
28 |
29 | assert run_result.exit_code == 0
30 |
--------------------------------------------------------------------------------
/tests/test_graph.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from depx.graph import create_graph_from, to_html, to_graphml, to_dotfile, to_json
4 | import networkx as nx
5 | import io
6 | import os
7 | import pytest
8 |
9 |
10 | @pytest.fixture
11 | def dependencies():
12 | return [
13 | {
14 | "from_module": "opportunity.models",
15 | "to_module": "common.utils",
16 | "category": "local",
17 | },
18 | {
19 | "from_module": "opportunity.models",
20 | "to_module": "common.utils",
21 | "category": "local",
22 | },
23 | {
24 | "from_module": "sales_appoinment.views",
25 | "to_module": "common.utils",
26 | "category": "local",
27 | },
28 | {
29 | "from_module": "common.utils",
30 | "to_module": "opportunity.views",
31 | "category": "local",
32 | },
33 | ]
34 |
35 |
36 | def test_create_graph(dependencies):
37 | graph = create_graph_from(dependencies)
38 |
39 | expected_nodes = [
40 | 'opportunity.models',
41 | 'common.utils',
42 | 'sales_appoinment.views',
43 | 'opportunity.views',
44 | ]
45 | expected_edges = [
46 | ('opportunity.models', 'common.utils', {'weight': 2}),
47 | ('common.utils', 'opportunity.views', {'weight': 1}),
48 | ('sales_appoinment.views', 'common.utils', {'weight': 1}),
49 | ]
50 | for node in graph.nodes():
51 | assert node in expected_nodes
52 | expected_nodes.remove(node)
53 |
54 | assert expected_nodes == []
55 |
56 | for edge in graph.edges(data=True):
57 | assert edge in expected_edges
58 | expected_edges.remove(edge)
59 |
60 | assert expected_edges == []
61 |
62 |
63 | def test_format_to_html(dependencies):
64 | graph = create_graph_from(dependencies)
65 |
66 | content = to_html(graph=graph, path=os.getcwd())
67 |
68 | assert '' in content
69 |
70 |
71 | def test_format_to_graphml(dependencies):
72 | graph = create_graph_from(dependencies)
73 | content = to_graphml(graph=graph, path=os.getcwd())
74 |
75 | exported_graph = nx.read_graphml(io.StringIO(content))
76 |
77 | assert exported_graph.nodes() == graph.nodes()
78 | assert exported_graph.edges() == graph.edges()
79 |
80 |
81 | def test_format_to_dotfile(dependencies):
82 | graph = create_graph_from(dependencies)
83 | content = to_dotfile(graph=graph, path=os.getcwd())
84 |
85 | exported_graph = nx.drawing.nx_pydot.read_dot(io.StringIO(content))
86 |
87 | assert exported_graph.nodes() == graph.nodes()
88 | assert (
89 | nx.to_dict_of_dicts(graph).keys() == nx.to_dict_of_dicts(exported_graph).keys()
90 | )
91 |
92 |
93 | def test_format_to_json(dependencies):
94 | content = to_json(dependencies=dependencies)
95 |
96 | assert json.loads(content) == dependencies
97 |
--------------------------------------------------------------------------------
/tests/test_parsing.py:
--------------------------------------------------------------------------------
1 | from operator import itemgetter
2 |
3 | from depx.parsing import (
4 | find_imports_from_text,
5 | _dependency,
6 | find_imports,
7 | _is_package,
8 | _is_module,
9 | _find_base_name,
10 | filter_top_level_names,
11 | )
12 | import os
13 | import pytest
14 | import sys
15 | from textwrap import dedent
16 |
17 |
18 | @pytest.mark.parametrize(
19 | 'base_name,text,expected',
20 | [
21 | (
22 | 'SINGLE_IMPORT',
23 | 'import some_module',
24 | [
25 | _dependency(
26 | from_module='SINGLE_IMPORT',
27 | from_name='some_module',
28 | to_module='some_module',
29 | to_name='some_module',
30 | category='module',
31 | ),
32 | ],
33 | ),
34 | (
35 | 'ALIAS_IMPORT',
36 | 'import some_module as alias_name',
37 | [
38 | _dependency(
39 | from_module='ALIAS_IMPORT',
40 | from_name='alias_name',
41 | to_module='some_module',
42 | to_name='some_module',
43 | category='module',
44 | ),
45 | ],
46 | ),
47 | (
48 | 'MULTI_IMPORT_ONE_LINE',
49 | 'import some_module, other_module as other_alias',
50 | [
51 | _dependency(
52 | from_module='MULTI_IMPORT_ONE_LINE',
53 | from_name='some_module',
54 | to_module='some_module',
55 | to_name='some_module',
56 | category='module',
57 | ),
58 | _dependency(
59 | from_module='MULTI_IMPORT_ONE_LINE',
60 | from_name='other_alias',
61 | to_module='other_module',
62 | to_name='other_module',
63 | category='module',
64 | ),
65 | ],
66 | ),
67 | (
68 | 'FROM_NAMESPACE_IMPORT_NAME',
69 | 'from some_namespace import some_name',
70 | [
71 | _dependency(
72 | from_module='FROM_NAMESPACE_IMPORT_NAME',
73 | from_name='some_name',
74 | to_module='some_namespace',
75 | to_name='some_name',
76 | category='module',
77 | is_relative=False,
78 | level=0,
79 | ),
80 | ],
81 | ),
82 | (
83 | 'FROM_NAMESPACE_IMPORT_NAME_ALIAS',
84 | 'from some_namespace import some_name as alias_name',
85 | [
86 | _dependency(
87 | from_module='FROM_NAMESPACE_IMPORT_NAME_ALIAS',
88 | from_name='alias_name',
89 | to_module='some_namespace',
90 | to_name='some_name',
91 | category='module',
92 | is_relative=False,
93 | level=0,
94 | ),
95 | ],
96 | ),
97 | (
98 | 'BASE_PACKAGE.RELATIVE_IMPORT',
99 | 'from . import some_module',
100 | [
101 | _dependency(
102 | from_module='BASE_PACKAGE.RELATIVE_IMPORT',
103 | from_name='some_module',
104 | to_module='BASE_PACKAGE.some_module',
105 | to_name='some_module',
106 | category='module',
107 | is_relative=True,
108 | level=1,
109 | ),
110 | ],
111 | ),
112 | (
113 | 'BASE_PACKAGE.RELATIVE_IMPORT_TO_SIBLING',
114 | 'from .sibling_namespace import some_name',
115 | [
116 | _dependency(
117 | from_module='BASE_PACKAGE.RELATIVE_IMPORT_TO_SIBLING',
118 | from_name='some_name',
119 | to_module='BASE_PACKAGE.sibling_namespace',
120 | to_name='some_name',
121 | category='module',
122 | is_relative=True,
123 | level=1,
124 | ),
125 | ],
126 | ),
127 | (
128 | 'RELATIVE_IMPORT_FROM_ROOT',
129 | 'from . import some_module',
130 | [
131 | _dependency(
132 | from_module='RELATIVE_IMPORT_FROM_ROOT',
133 | from_name='some_module',
134 | to_module='some_module',
135 | to_name='some_module',
136 | category='module',
137 | is_relative=True,
138 | level=1,
139 | ),
140 | ],
141 | ),
142 | (
143 | 'LOCAL_IMPORT',
144 | """
145 | def function():
146 | import some_module
147 | """,
148 | [
149 | _dependency(
150 | from_module='LOCAL_IMPORT',
151 | from_name='some_module',
152 | to_module='some_module',
153 | to_name='some_module',
154 | category='local',
155 | ),
156 | ],
157 | ),
158 | ],
159 | )
160 | def test_find_imports_from_text(base_name, text, expected):
161 | text = dedent(text)
162 | assert list(find_imports_from_text(text, base_name)) == expected
163 |
164 |
165 | @pytest.mark.skipif(sys.version_info < (3, 5), reason='async not in older Pythons')
166 | def test_find_imports_from_text__async_def():
167 | imports = list(
168 | find_imports_from_text(
169 | dedent(
170 | """
171 | async def function():
172 | import some_module
173 | """
174 | ),
175 | base_name='TEST_MODULE',
176 | )
177 | )
178 |
179 | assert imports == [
180 | _dependency(
181 | from_module='TEST_MODULE',
182 | from_name='some_module',
183 | to_module='some_module',
184 | to_name='some_module',
185 | category='local',
186 | ),
187 | ]
188 |
189 |
190 | @pytest.mark.parametrize(
191 | 'path,expected',
192 | [
193 | (os.getcwd() + '/tests/fake_project/another_fake_module', True),
194 | (os.getcwd() + '/tests/fake_project/fake_module', True),
195 | (os.getcwd() + '/tests/fake_project/random_folder', False),
196 | ],
197 | )
198 | def test_is_package(path, expected):
199 | assert _is_package(path) is expected
200 |
201 |
202 | def test_find_imports():
203 | expected = [
204 | {
205 | 'from_module': 'another_fake_module.missing_creativity',
206 | 'to_module': 'fake_module.another_module',
207 | 'category': 'module',
208 | 'is_relative': False,
209 | 'from_name': 'oh_nice',
210 | 'to_name': 'oh_nice',
211 | 'level': 0,
212 | },
213 | {
214 | 'from_module': 'another_fake_module.missing_creativity',
215 | 'to_module': 'random', # FIXME identify as internal
216 | 'category': 'module',
217 | 'is_relative': False,
218 | 'from_name': 'random',
219 | 'to_name': 'random',
220 | 'level': 0,
221 | },
222 | {
223 | 'from_module': 'fake_module.__init__',
224 | 'to_module': 'fake_module.a_module',
225 | 'category': 'module',
226 | 'is_relative': False,
227 | 'from_name': 'fake_module.a_module',
228 | 'to_name': 'fake_module.a_module',
229 | 'level': 0,
230 | },
231 | {
232 | 'from_module': 'fake_module.__init__',
233 | 'to_module': 'fake_module',
234 | 'category': 'module',
235 | 'is_relative': False,
236 | 'from_name': 'another_module',
237 | 'to_name': 'another_module',
238 | 'level': 0,
239 | },
240 | {
241 | 'from_module': 'fake_module.a_module',
242 | 'to_module': 'fake_module.another_module',
243 | 'category': 'module',
244 | 'is_relative': True,
245 | 'from_name': 'oh_nice',
246 | 'to_name': 'oh_nice',
247 | 'level': 1,
248 | },
249 | ]
250 | by_name = itemgetter('from_module', 'from_name', 'to_module', 'to_name')
251 | assert sorted(find_imports('tests/fake_project'), key=by_name) == sorted(
252 | expected, key=by_name
253 | )
254 |
255 |
256 | @pytest.mark.parametrize(
257 | 'path,expected',
258 | [
259 | (os.getcwd() + '/tests/fake_project/another_fake_module/__init__.py', True),
260 | (
261 | os.getcwd()
262 | + '/tests/fake_project/another_fake_module/missing_creativity.py',
263 | True,
264 | ),
265 | (os.getcwd() + '/tests/fake_project/setup.py', True),
266 | (
267 | os.getcwd()
268 | + '/tests/fake_project/random_folder/python_file_without_py_extension.txt',
269 | True,
270 | ),
271 | (os.getcwd() + '/tests/fake_project/random_folder/cool_file.txt', False),
272 | ],
273 | )
274 | def test_is_module(path, expected):
275 | assert _is_module(path) is expected
276 |
277 |
278 | @pytest.mark.parametrize(
279 | 'path,expected',
280 | [
281 | ('tests/fake_project/another_fake_module/__init__.py', 'another_fake_module'),
282 | (
283 | os.getcwd()
284 | + '/tests/fake_project/another_fake_module/missing_creativity.py',
285 | 'another_fake_module',
286 | ),
287 | ('tests/fake_project/setup.py', ''),
288 | ],
289 | )
290 | def test_find_base_name(path, expected):
291 | assert _find_base_name(path) == expected
292 |
293 |
294 | def test_filter_top_level_names():
295 | dependency = {
296 | 'from_module': 'a.b',
297 | 'to_module': 'x.y.z',
298 | 'from_name': 'SOMETHING',
299 | 'to_name': 'SOMETHING ELSE',
300 | }
301 | expected = {
302 | 'from_module': 'a',
303 | 'to_module': 'x',
304 | 'from_name': '',
305 | 'to_name': '',
306 | }
307 | assert list(filter_top_level_names([dependency])) == [expected]
308 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py36, py37, py38, py39, flake8
3 | isolated_build = true
4 |
5 | [travis]
6 | python =
7 | 3.9: py39
8 | 3.8: py38
9 | 3.7: py37
10 | 3.6: py36
11 |
12 | [testenv:flake8]
13 | basepython = python
14 | deps = flake8
15 | commands = flake8 depx
16 |
17 | [testenv]
18 | deps = pytest
19 | commands = pytest --basetemp={envtmpdir}
20 |
--------------------------------------------------------------------------------