[
  {
    "path": ".gitignore",
    "content": "# Own\n*.json\n\n\n# Created by https://www.gitignore.io/api/python\n\n### Python ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*,cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# dotenv\n.env\n\n# virtualenv\n.venv\nvenv/\nENV/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# End of https://www.gitignore.io/api/python\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\nCopyright (c) 2017 Peter Goldsborough\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include *.md\ninclude LICENSE\n\nrecursive-include www *\nrecursive-include * Makefile\nrecursive-exclude * __pycache__\nrecursive-exclude * *.py[co]\nrecursive-exclude www/sigma/sigma.js/ *\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: clean-pyc clean-build docs clean\n\nhelp:\n\t@echo \"clean - remove all build, test, coverage and Python artifacts.\"\n\t@echo \"clean-build - remove build artifacts.\"\n\t@echo \"clean-pyc - remove Python file artifacts.\"\n\t@echo \"bump - bumps the version.\"\n\t@echo \"test-register - register the project at TestPyPI.\"\n\t@echo \"register - register the project at PyPI.\"\n\t@echo \"test-upload - package and upload a releaes to TestPyPI.\"\n\t@echo \"upload - package and upload a upload to PyPI.\"\n\t@echo \"dist - package.\"\n\t@echo \"install - install the package to the active Python's site-packages.\"\n\nclean: clean-build clean-pyc\n\nclean-build:\n\trm -rf build/\n\trm -rf dist/\n\trm -rf .eggs/\n\tfind . -name '*.egg-info' -exec rm -rf {} +\n\tfind . -name '*.egg' -exec rm -f {} +\n\nclean-pyc:\n\tfind . -name '*.pyc' -exec rm -f {} +\n\tfind . -name '*.pyo' -exec rm -f {} +\n\tfind . -name '*~' -exec rm -f {} +\n\tfind . -name '__pycache__' -exec rm -rf {} +\n\nbump:\n\tpython -m scripts.bump\n\ndist: clean\n\tpython setup.py sdist\n\tpython setup.py bdist_wheel\n\ntest-register:\n\tpython setup.py register -r test\n\nregister:\n\tpython setup.py register\n\ntest-upload: dist\n\ttwine upload -r test $(wildcard dist/*)\n\nupload: dist\n\ttwine upload $(wildcard dist/*)\n\ninstall:\n\tpython setup.py install\n"
  },
  {
    "path": "README.md",
    "content": "# :fireworks: ig\n\n<p align=\"center\">\n  <img src=\"extra/graph.gif\">\n  <br><br>\n  <code>ig</code> is a tool to interactively visualize include graphs for C++ projects\n  <br><br>\n  <img alt=\"license\" src=\"https://img.shields.io/github/license/mashape/apistatus.svg\"/>\n</p>\n\n## Overview\n\nPoint `ig` at any directory containing C++ source or header files and it will\nconstruct a full graph of all includes, serve you a local website and visualize\nthe graph interactively with [sigma.js](http://sigmajs.org), for you to admire.\n\nUsage is very easy:\n\n```sh\n$ ig -o include\n```\n\nwill inspect the folder `include`, serve a website on `localhost:8080` and even\nopen your browser for you. The full set of options currently include:\n\n```sh\nusage: ig [-h] [--pattern PATTERNS] [-i PREFIXES] [-v] [-p PORT] [-o] [-j]\n          [-d DIRECTORY] [--relation {includes,included-by}]\n          [--min-degree MIN_DEGREE] [--group-granularity GROUP_GRANULARITY]\n          [--full-path] [--colors COLORS] [--color-variation COLOR_VARIATION]\n          [--color-alpha-min COLOR_ALPHA_MIN]\n          directories [directories ...]\n\nVisualize C++ include graphs\n\npositional arguments:\n  directories           The directories to inspect\n\noptional arguments:\n  -h, --help            show this help message and exit\n  --pattern PATTERNS    The file (glob) patterns to look for\n  -i PREFIXES, -I PREFIXES, --prefix PREFIXES\n                        An include path for headers to recognize\n  -v, --verbose         Turn on verbose output\n  -p PORT, --port PORT  The port to serve the visualization on\n  -o, --open            Open the webpage immediately\n  -j, --json            Print the graph JSON and instead of serving it\n  -d DIRECTORY, --dir DIRECTORY\n                        The directory to store the served files in. If not\n                        supplied, a temporary directory is created.\n  --relation {includes,included-by}\n                        The relation of edges in the graph\n  --min-degree MIN_DEGREE\n                        The initial minimum degree nodes should have to be\n                        displayed\n  --group-granularity GROUP_GRANULARITY\n                        How coarse to group nodes (by folder)\n  --full-path           If set, shows the full path for nodes\n  --colors COLORS       The base RGB colors separated by commas\n  --color-variation COLOR_VARIATION\n                        The variation in RGB around the base colors\n  --color-alpha-min COLOR_ALPHA_MIN\n                        The minimum alpha value for colors\n```\n\nBut does it scale? It scales quite well. The graph you see above is the include\ngraph for the entire LLVM and clang codebase, which spans more than 5,000 files\nand 1.5M LOC. Note that the visualization also includes sliders to group nodes\nby folder and filter out low-degree nodes.\n\n## Installation\n\nGet it with pip:\n\n```sh\n$ pip install ig-cpp\n```\n\nWorks with Python 2 and 3.\n\n## Examples\n\nWho ever said C++ was an ugly language?\n\n<p align=\"center\">\n  <img src=\"extra/llvm-adt.png\">\n  <br><br>\n  <b>LLVM/ADT</b>\n  <br><br>\n</p>\n\n<p align=\"center\">\n  <img src=\"extra/tf.png\">\n  <br><br>\n  <b>TensorFlow</b>\n  <br><br>\n</p>\n\n<p align=\"center\">\n  <img src=\"extra/libcxx.png\">\n  <br><br>\n  <b>libc++ (the standard library)</b>\n  <br><br>\n</p>\n\n## Authors\n\n[Peter Goldsborough](http://goldsborough.me) + [cat](https://goo.gl/IpUmJn)\n:heart:\n"
  },
  {
    "path": "ig/__init__.py",
    "content": "from datetime import date\n\n__title__ = 'ig'\n__url__ = \"https://github.com/goldsborough/ig\"\n__version__ = '0.1.9'\n__author__ = 'Peter Goldsborough'\n__license__ = 'MIT'\n__copyright__ = 'Copyright {0} Peter Goldsborough'.format(date.today().year)\n"
  },
  {
    "path": "ig/colors.py",
    "content": "import random\n\n\ndef random_color(base, variation):\n    '''\n    Returns a random, bounded color value.\n\n    Args:\n        base: Some base color component (between 0 and 255)\n        variation: The degree of variation (around the color)\n\n    Returns:\n        A random color.\n    '''\n    color = base + (2 * random.random() - 1) * variation\n    return max(8, min(int(color), 256))\n\n\nclass Colors(object):\n    '''\n    Aggregates information about the color scheme of the visualization.\n    '''\n\n    def __init__(self, base_colors):\n        '''\n        Constructor.\n\n        Args:\n            base_colors: The base colors around which to vary\n        '''\n        self.base = list(base_colors)\n        self.variation = None\n        self.alpha_min = None\n\n    def generate(self):\n        '''\n        Generates a color.\n\n        Returns:\n            A new RGBA color value.\n        '''\n        rgba = [random_color(color, self.variation) for color in self.base]\n        rgba.append(max(self.alpha_min, random.random()))\n        return 'rgba({0})'.format(','.join(map(str, rgba)))\n"
  },
  {
    "path": "ig/graph.py",
    "content": "''' Defines the graph structure that stores the include relationships. '''\n\nimport logging\nimport os\nimport random\n\n\nlog = logging.getLogger(__name__)\n\n\nclass Graph(object):\n    '''\n    Stores nodes (files) and edges (includes).\n\n    Nodes are stored in a map from absolute filename to a dictionary, which\n    holds all the information required by sigma.js (e.g. ID and label). Edges\n    are stored as a list of (id, source, target) objects, along with some\n    additional information. The representation is not very compact or optimal,\n    but processing even very large projects still seems instant.\n    '''\n    def __init__(self,\n                 relation,\n                 full_path,\n                 colors,\n                 group_granularity):\n        '''\n        Constructor.\n\n        Args:\n            relation: One of {'includes', 'included-by'}\n            full_path: Whether to print node labels with their full path\n            colors: A `Colors` object storing information about the color scheme\n            group_granularity: The granularity setting for node groups\n            directory: The directory from which to serve the visualization.\n        '''\n        assert relation in ('includes', 'included-by')\n\n        self.edges = []\n        self.nodes = {}\n        self.is_included_by_relation = (relation == 'included-by')\n        self.use_full_path = full_path\n        self.colors = colors\n        self.group_granularity = group_granularity\n\n    def add(self, node_name, neighbors):\n        '''\n        Adds a new node to the graph, along with its adjacent neighbors.\n\n        Args:\n            node_name: The name of the node (i.e. current file)\n            neighbors: A list of names of neighbors (included files)\n        '''\n        node = self._get_or_add_node(node_name)\n\n        if not self.is_included_by_relation:\n            node['size'] = len(neighbors)\n\n        for neighbor_name in neighbors:\n            neighbor = self._get_or_add_node(neighbor_name)\n            if self.is_included_by_relation:\n                neighbor['size'] += 1\n            self._add_edge(node, neighbor)\n\n    def to_json(self):\n        '''\n        Turns the graph into a dictionary (a.k.a. JSON).\n\n        The format is {\"nodes\": list of node objects, \"edges\": array of edge\n        objects}.\n\n        Returns:\n            A JSON (dictionary) representation of the graph.\n        '''\n        nodes = list(self.nodes.values())\n        return dict(nodes=nodes, edges=self.edges)\n\n    @property\n    def is_empty(self):\n        '''\n        Returns:\n            True if the graph has no nodes at all, else False.\n        '''\n        return len(self.nodes) == 0\n\n    def _get_or_add_node(self, node_name):\n        '''\n        Returns a node and possibly adds it to the graph.\n\n        Args:\n            node_name: The name of the node to fetch\n\n        Returns:\n            The node entry for the given name.\n        '''\n        node = self.nodes.get(node_name)\n        if node is None:\n            node = self._add_node(node_name)\n        return node\n\n    def _add_node(self, node_name):\n        '''\n        Adds a node to the graph.\n\n        Args:\n            node_name: The name of the node to add\n\n        Returns:\n            The newly created node object.\n        '''\n        assert node_name not in self.nodes\n\n        node = {}\n        node['id'] = len(self.nodes)\n        node['size'] = 1\n        node['color'] = self.colors.generate()\n\n        if self.use_full_path:\n            node['label'] = node_name\n        else:\n            node['label'] = os.path.basename(node_name)\n\n        # Take up to the last two directory names as the group\n        directories = os.path.dirname(node_name).split(os.sep)\n        begin = len(directories) - self.group_granularity\n        node['group'] = os.sep.join(directories[begin:begin + 2])\n\n        # Make the initial starting point random, but very small, so we get\n        # an \"explosion\"/\"expansion\" effect.\n        node['x'] = random.random() * 0.01\n        node['y'] = random.random() * 0.01\n\n        self.nodes[node_name] = node\n\n        return node\n\n    def _add_edge(self, source, target):\n        '''\n        Adds an edge to the graph.\n\n        Args:\n            source: The entry of the source node\n            target: The entry of the target node\n\n        Returns:\n            The newly created edge object\n        '''\n        edge = {}\n        edge['id'] = len(self.edges)\n        edge['size'] = 10  # Make the arrows larger?\n        edge['type'] = 'curvedArrow'\n\n        # The natural direction is \"includes\", so swap if we want \"included-by\"\n        if self.is_included_by_relation:\n            source, target = target, source\n        edge['source'] = source['id']\n        edge['target'] = target['id']\n\n        self.edges.append(edge)\n\n        return edge\n\n    def __repr__(self):\n        '''\n        Returns:\n            A string representation of the graph.\n        '''\n        nodes = len(self.nodes)\n        edges = len(self.edges)\n        return '<Graph: nodes = {0}, edges = {1}>'.format(nodes, edges)\n"
  },
  {
    "path": "ig/main.py",
    "content": "'''Entry point and command line parsing for ig.'''\n\nfrom __future__ import print_function\n\nimport argparse\nimport logging\nimport os\nimport sys\n\nfrom ig import colors, graph, serve, walk\n\n\ndef setup_logging():\n    '''Sets up the root logger.'''\n    handler = logging.StreamHandler(sys.stderr)\n    formatter = logging.Formatter('[%(levelname)s] %(message)s')\n    handler.setFormatter(formatter)\n\n    log = logging.getLogger(__package__)\n    log.addHandler(handler)\n    log.setLevel(logging.INFO)\n\n    return log\n\n\ndef parse_arguments(args):\n    '''\n    Sets up the command line argument parser and parses arguments.\n\n    Args:\n        args: The list of argumnets passed to the command line\n\n    Returns:\n        The parsed arguments.\n    '''\n    parser = argparse.ArgumentParser(description='Visualize C++ include graphs')\n    parser.add_argument('directories',\n                        nargs='+',\n                        help='The directories to inspect')\n    parser.add_argument('--pattern',\n                        action='append',\n                        default=['*.[ch]pp', '*.[ch]'],\n                        dest='patterns',\n                        help='The file (glob) patterns to look for')\n    parser.add_argument('-i', '-I', '--prefix',\n                        action='append',\n                        dest='prefixes',\n                        default=[os.getcwd()],\n                        help='An include path for headers to recognize')\n    parser.add_argument('-v', '--verbose',\n                        action='store_true',\n                        help='Turn on verbose output')\n\n    parser.add_argument('-p', '--port',\n                        type=int,\n                        default=8080,\n                        help='The port to serve the visualization on')\n    parser.add_argument('-o', '--open',\n                        action='store_true',\n                        help='Open the webpage immediately')\n    parser.add_argument('-j', '--json',\n                        action='store_true',\n                        help='Print the graph JSON instead of serving it')\n    parser.add_argument('-d', '--dir',\n                        dest='directory',\n                        help='The directory to store the served files in. If '\n                             'not supplied, a temporary directory is created.')\n\n    parser.add_argument('--relation',\n                        choices=['includes', 'included-by'],\n                        default='included-by',\n                        help='The relation of edges in the graph')\n    parser.add_argument('--min-degree',\n                        type=float,\n                        default=0.1,\n                        help='The initial minimum degree nodes should have to '\n                             'be displayed')\n    parser.add_argument('--group-granularity',\n                        type=int,\n                        default=2,\n                        help='How coarse to group nodes (by folder)')\n    parser.add_argument('--full-path',\n                        action='store_true',\n                        help='If set, shows the full path for nodes')\n    parser.add_argument('--colors',\n                        type=lambda p: colors.Colors(map(int, p.split(','))),\n                        default='234, 82, 77',\n                        help='The base RGB colors separated by commas')\n    parser.add_argument('--color-variation',\n                        type=int,\n                        default=200,\n                        help='The variation in RGB around the base colors')\n    parser.add_argument('--color-alpha-min',\n                        type=float,\n                        default=0.7,\n                        help='The minimum alpha value for colors')\n\n    args = parser.parse_args(args)\n\n    # Necessary for standard includes\n    args.prefixes.append('')\n\n    if not (0 <= args.color_alpha_min <= 1):\n        raise RuntimeError('--color-alpha-min must be in interval [0, 1]')\n\n    args.colors.variation = args.color_variation\n    args.colors.alpha_min = args.color_alpha_min\n\n    return args\n\n\ndef make_json(args, graph_json):\n    '''\n    Creates the JSON payload for the visualization.\n\n    Args:\n        args: The command line arguments.\n        graph_json: The JSON dict from the graph.\n\n    Returns:\n        The payload.\n    '''\n    if args.json:\n        print(graph_json)\n        sys.exit(0)\n\n    # Additional settings to configure the visualization\n    settings = dict(initialDegree=args.min_degree)\n\n    return dict(settings=settings, graph=graph_json)\n\n\ndef main():\n    log = setup_logging()\n\n    args = parse_arguments(sys.argv[1:])\n    if args.verbose:\n        log.setLevel(logging.DEBUG)\n    log.debug('Received arguments: %s', args)\n\n    include_graph = graph.Graph(args.relation,\n                                args.full_path,\n                                args.colors,\n                                args.group_granularity)\n    walk.walk(include_graph, args)\n\n    if include_graph.is_empty:\n        log.debug('Could not find a single node, exiting')\n        sys.exit(-1)\n\n    json = make_json(args, include_graph.to_json())\n\n    with serve.Server(args.directory) as server:\n        server.write(json)\n        server.run(args.open, args.port)\n\n    log.info('Shutting down')\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "ig/paths.py",
    "content": "'''Handles searching for the WWW path and copying operations.'''\n\nimport logging\nimport os\nimport shutil\nimport tempfile\n\nlog = logging.getLogger(__name__)\n\n\nWWW = os.path.join(os.path.dirname(os.path.realpath(__file__)),\n                   os.pardir,\n                   'www')\n\nif not os.path.exists(WWW):\n    message = 'Could not find www directory for ig: {0}'\n    raise EnvironmentError(message.format(WWW))\n\n\ndef create_directory(directory):\n    '''\n    (Maybe) creates a directory and copies the `www` folder to it.\n\n    Args:\n        directory: Optionally, an existing directory to copy to.\n\n    Returns:\n        The path of the possibly created directory.\n    '''\n    if directory is None:\n        directory = tempfile.mkdtemp(prefix='ig-')\n        log.debug('Created temporary directory %s', directory)\n\n    # Has to not exist for copytree\n    if os.path.exists(directory):\n        shutil.rmtree(directory)\n\n    shutil.copytree(WWW, directory)\n\n    log.debug('Copied contents of www folder to %s', directory)\n\n    return directory\n"
  },
  {
    "path": "ig/serve.py",
    "content": "'''The server that serves the web visualization.'''\n\nimport json\nimport logging\nimport os\nimport shutil\nimport socket\nimport webbrowser\n\nfrom ig import paths\n\ntry:\n    import socketserver\n    import http.server as http\nexcept ImportError:\n    import SocketServer as socketserver\n    import SimpleHTTPServer as http\n\n\nlog = logging.getLogger(__name__)\n\n\nclass Server(object):\n    def __init__(self, directory):\n        '''\n        Constructor.\n\n        Args:\n            directory: The directory to serve from.\n        '''\n        self.delete_directory = directory is None\n        self.directory = paths.create_directory(directory)\n\n    def run(self, open_immediately, port):\n        '''\n        Serves the `www` directory.\n\n        Args:\n            open_immediately: Whether to open the web browser immediately\n            port: The port at which to serve the graph\n        '''\n        os.chdir(self.directory)\n        handler = http.SimpleHTTPRequestHandler\n        handler.extensions_map.update({\n            '.webapp': 'application/x-web-app-manifest+json',\n        })\n\n        server = socketserver.TCPServer(('', port), handler)\n        server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n\n        address = 'http://localhost:{0}/graph.html'.format(port)\n        log.info('Serving at %s', address)\n\n        if open_immediately:\n            log.debug('Opening webbrowser')\n            webbrowser.open(address)\n\n        server.serve_forever()\n\n    def write(self, payload):\n        '''\n        Writes the given JSON representation to the served location.\n\n        Args:\n            payload: The playlod to JSONify and store.\n        '''\n        path = os.path.join(self.directory, 'graph.json')\n        with open(path, 'w') as graph_file:\n            graph_file.write(json.dumps(payload, indent=4))\n\n        log.debug('Wrote graph file to {0}'.format(path))\n\n    def cleanup(self):\n        if self.delete_directory:\n            assert self.directory is not None\n            shutil.rmtree(self.directory, ignore_errors=True)\n            log.debug('Deleted directory %s', self.directory)\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, error_type, error_value, traceback):\n        self.cleanup()\n        if error_type == KeyboardInterrupt:\n            return True  # Supresses the exception\n        # Any other exception is propagated up\n"
  },
  {
    "path": "ig/walk.py",
    "content": "from __future__ import print_function\n\nimport fnmatch\nimport logging\nimport os\nimport re\nimport sys\n\nlog = logging.getLogger(__name__)\n\n\nINCLUDE_PATTERN = re.compile(r'^#include [\"<](.*)[\">]$')\n\n\ndef try_prefixes(path, prefixes):\n    '''\n    Tries to prepend a list of prefixes to a path to see if any exists.\n\n    This is necessary to ensure that we have unique file paths, even if the same\n    file is specified differently (sometimes relative to one directory, then to\n    another etc.).\n\n    Args:\n        path: The path to append to the prefixes\n        perfixes: The prefixes to prepend to the path\n\n    Returns:\n        The best possible path.\n    '''\n    for prefix in prefixes:\n        full_path = os.path.realpath(os.path.join(prefix, path))\n        if os.path.exists(full_path):\n            return full_path\n\n    return path\n\n\ndef get_includes(filename, prefixes):\n    '''\n    Parses out the includes from a file.\n\n    Args:\n        filename: The name of the file to get includes for\n        prefixes: The prefixes under which to search for includes\n\n    Returns:\n        A list of includes for the file.\n    '''\n    includes = set()\n    with open(filename) as source:\n        for line in source:\n            match = INCLUDE_PATTERN.match(line)\n            if match is not None:\n                full_path = try_prefixes(match.group(1), prefixes)\n                includes.add(full_path)\n\n    return includes\n\n\ndef glob(directory, pattern):\n    '''\n    Globs for files patterns under a directory.\n\n    There is a `glob` module, but its recursive variant only works in Python3.\n    This is a short DIY version of recursive globbing.\n\n    Args:\n        directory: The root directory\n        pattern: The pattern to glob for\n\n    Yields:\n        Any matching files (with absolute paths).\n    '''\n    for root, _, filenames in os.walk(directory):\n        for filename in fnmatch.filter(filenames, pattern):\n            yield os.path.join(root, filename)\n\n\ndef walk(graph, args):\n    '''\n    Walks the file tree, populating the graph.\n\n    Args:\n        graph: The empty graph to populate\n        args: The arguments passed to the command line\n\n    Returns:\n        The (possibly) populated graph.\n    '''\n    for directory in args.directories:\n        # Swap pattern <-> filename loops if too inefficient\n        for pattern in args.patterns:\n            path = os.path.realpath(directory)\n            for filename in glob(path, pattern):\n                if os.path.isdir(filename):\n                    log.debug('%s is a directory, skipping', filename)\n                    continue\n                includes = get_includes(filename, [path] + args.prefixes)\n                graph.add(filename, includes)\n\n    log.debug('Resulting graph: %s', repr(graph))\n"
  },
  {
    "path": "scripts/__init__.py",
    "content": ""
  },
  {
    "path": "scripts/bump.py",
    "content": "\"\"\"Version-bumping script.\"\"\"\n\nfrom __future__ import print_function\n\nimport os.path\nimport re\n\ndef bump(match):\n    \"\"\"Bumps the version\"\"\"\n    before, old_version, after = match.groups()\n    major, minor, patch = map(int, old_version.split('.'))\n    patch += 1\n    if patch == 10:\n        patch = 0\n        minor += 1\n        if minor == 10:\n            minor = 0\n            major += 1\n    new_version = '{0}.{1}.{2}'.format(major, minor, patch)\n\n    print('{0} => {1}'.format(old_version, new_version))\n\n    return before + new_version + after\n\n\ndef main():\n    root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n    project = os.path.split(root)[-1]\n    init_path = os.path.join(root, project, '__init__.py')\n    with open(init_path) as source:\n        original = source.read()\n    pattern = re.compile(r'^(\\s*__version__\\s*=\\s*[\\'\"])([^\\'\"]*)([\\'\"])', re.M)\n    replaced = re.sub(pattern, bump, original)\n    if replaced == original:\n        raise RuntimeError('Could not find version!')\n    with open(init_path, 'w') as destination:\n        destination.write(replaced)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "setup.cfg",
    "content": "[wheel]\nuniversal=1\n"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\n\"\"\"\nsetup.py script for setuptools.\n\"\"\"\n\nimport re\n\nfrom setuptools import setup, find_packages\n\nwith open('ig/__init__.py') as init:\n    text = init.read()\n    match = re.search(r'^__version__\\s*=\\s*[\\'\"]([^\\'\"]*)[\\'\"]', text, re.M)\n    version = match.group(1)\n\nwith open('README.md') as readme:\n    long_description = readme.read()\n\nsetup(\n    name='ig-cpp',\n    version=version,\n\n    description='A tool to visualize include graphs for C++ projects',\n    long_description=long_description,\n\n    url=\"https://github.com/goldsborough/ig\",\n    license='MIT',\n\n    author='Peter Goldsborough',\n    author_email='peter@goldsborough.me',\n\n    classifiers=[\n        'Development Status :: 5 - Production/Stable',\n\n        'Intended Audience :: Developers',\n        'Topic :: Software Development',\n\n        'License :: OSI Approved :: MIT License',\n\n        'Programming Language :: Python',\n        'Programming Language :: Python :: 2',\n        'Programming Language :: Python :: 2.6',\n        'Programming Language :: Python :: 2.7',\n        'Programming Language :: Python :: 3',\n        'Programming Language :: Python :: 3.2',\n        'Programming Language :: Python :: 3.3',\n        'Programming Language :: Python :: 3.4',\n        'Programming Language :: Python :: 3.5'\n    ],\n    keywords='visualization C++ tool',\n    packages=find_packages(exclude=['www']),\n    include_package_data=True,\n    package_data=dict(ig=[\n        '../README.md',\n        '../Makefile',\n        '../www/*',\n        '../www/sigma/*'\n    ]),\n\n    entry_points=dict(console_scripts=['ig = ig.main:main'])\n)\n"
  },
  {
    "path": "www/graph.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n\t<link href='http://fonts.googleapis.com/css?family=Lato:300,700' rel='stylesheet' type='text/css'>\n\t<link href='style.css' rel='stylesheet' type='text/css'>\n</head>\n<body>\n\t<div id=\"container\">\n\t\t</style>\n\t\t<div id=\"graph-container\"></div>\n\t\t<div id=\"control-pane\">\n\t\t\t<h2 class=\"underline\">filters</h2>\n\n\t\t\t<div>\n\t\t\t\t<h3>degree <span id=\"min-degree-val\">0</span></h3>\n          0\n          <input id=\"min-degree\" type=\"range\" min=\"0\" max=\"0\" value=\"0\">\n          <span id=\"max-degree-value\">0</span>\n          <br>\n\t\t\t</div>\n\t\t\t<span class=\"line\"></span>\n\t\t\t<div>\n\t\t\t\t<h3>groups</h3>\n\t\t\t\t<select id=\"node-group\">\n          <option value=\"\" selected>All</option>\n        </select>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\t<script src=\"sigma/sigma.min.js\"></script>\n\t<script src=\"sigma/sigma.layout.forceAtlas2.min.js\"></script>\n\t<script src=\"sigma/sigma.parsers.json.min.js\"></script>\n\t<script src=\"sigma/sigma.plugins.dragNodes.min.js\"></script>\n\t<script src=\"sigma/sigma.plugins.filter.min.js\"></script>\n\t<script src=\"sigma/sigma.canvas.edges.curvedArrow.js\"></script>\n\t<script src=\"sigma/sigma.canvas.labels.def.js\"></script>\n\t<script src=\"graph.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "www/graph.js",
    "content": "'use strict';\n\nconst $ = id => document.getElementById(id);\n\nfunction createFilter(instance, settings) {\n  // Initialize the Filter API\n  const filter = new sigma.plugins.filter(instance);\n\n  const maximumDegree = visualizePane(instance.graph, filter);\n\n  function applyMinDegreeFilter(value) {\n    if (typeof value === 'object') {\n      value = value.target.value;\n    }\n\n    $('min-degree').value = value;\n    $('min-degree-val').textContent = value;\n\n    filter\n      .undo('min-degree')\n      .nodesBy(node => instance.graph.degree(node.id) >= value, 'min-degree')\n      .apply();\n  }\n\n  function applyGroupFilter(element) {\n    let group = element.target[element.target.selectedIndex].value;\n    filter\n      .undo('node-group')\n      .nodesBy(node => !group || node.group === group, 'node-group')\n      .apply();\n  }\n\n  // for Chrome and FF\n  $('min-degree').addEventListener('input', applyMinDegreeFilter);\n  // for IE10+, that sucks\n  $('min-degree').addEventListener('change', applyMinDegreeFilter);\n  $('node-group').addEventListener('change', applyGroupFilter);\n\n  let degree = settings.initialDegree;\n  if (degree < 1) {\n    // Assume it's a fraction.\n    degree = Math.ceil(maximumDegree * degree);\n  }\n  applyMinDegreeFilter(Math.min(maximumDegree, degree));\n}\n\nfunction visualizePane(graph, filter) {\n  let maximumDegree = 0, categories = {};\n\n  // Collect the maximum degree and categories.\n  graph.nodes().forEach(node => {\n    maximumDegree = Math.max(maximumDegree, graph.degree(node.id));\n    categories[node.group] = true;\n  })\n\n  // Set the slider values.\n  $('min-degree').max = maximumDegree;\n  $('max-degree-value').textContent = maximumDegree;\n\n  // Set up the node group combo box.\n  const nodeGroup = $('node-group');\n  Object.keys(categories).forEach(function(group) {\n    if (group.length === 0) return;\n    let option = document.createElement('option');\n    option.text = group;\n    nodeGroup.add(option);\n  });\n\n  return maximumDegree;\n}\n\nfunction visualize(json) {\n  console.log(json);\n\n  const instance = new sigma({\n    graph: json.graph,\n    renderer: {\n      container: 'graph-container',\n      type: 'canvas',\n      skipErrors: true,\n      labelThreshold: 0,\n      labelSize: 'proportional'\n    }\n  });\n\n  instance.startForceAtlas2({\n    worker: true,\n    barnesHutOptimize: true,\n    adjustSizes: true,\n    slowDown: 20,\n    strongGravityMode: true\n  });\n\n  createFilter(instance, json.settings);\n\n  const drag = sigma.plugins.dragNodes(instance, instance.renderers[0]);\n  drag.bind('startdrag', event => {\n    if (instance.isForceAtlas2Running()) {\n      instance.killForceAtlas2()\n    }\n  });\n}\n\nconst xhr = new XMLHttpRequest();\nxhr.open('GET', 'graph.json');\nxhr.onreadystatechange = () => {\n  if (xhr.readyState === XMLHttpRequest.DONE) {\n    visualize(JSON.parse(xhr.responseText));\n  }\n}\n\nxhr.send();\n"
  },
  {
    "path": "www/sigma/LICENSE.txt",
    "content": "Copyright (C) 2013-2014, Alexis Jacomy, http://sigmajs.org\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"),\nto deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,\nand/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\nIN THE SOFTWARE.\n"
  },
  {
    "path": "www/sigma/README.md",
    "content": "[![Build Status](https://travis-ci.org/jacomyal/sigma.js.svg)](https://travis-ci.org/jacomyal/sigma.js)\n\nsigma.js - v1.2.0\n=================\n\nSigma is a JavaScript library dedicated to graph drawing, mainly developed by [@jacomyal](https://github.com/jacomyal) and [@Yomguithereal](https://github.com/Yomguithereal).\n\n### Resources\n\n[The website](http://sigmajs.org) provides a global overview of the project, and the documentation is available in the [Github Wiki](https://github.com/jacomyal/sigma.js/wiki).\n\nAlso, the `plugins` and `examples` directories contain various use-cases that might help you understand how to use sigma.\n\n### How to use it\n\nTo use it, clone the repository:\n\n```\ngit clone git@github.com:jacomyal/sigma.js.git\n```\n\nTo build the code:\n\n - Install [Node.js](http://nodejs.org/).\n - Install [gjslint](https://developers.google.com/closure/utilities/docs/linter_howto?hl=en).\n - Use `npm install` to install sigma development dependencies.\n - Use `npm run build` to minify the code with [Uglify](https://github.com/mishoo/UglifyJS). The minified file `sigma.min.js` will then be accessible in the `build/` folder.\n\nAlso, you can customize the build by adding or removing files from the `coreJsFiles` array in `Gruntfile.js` before applying the grunt task.\n\n### Contributing\n\nYou can contribute by submitting [issues tickets](http://github.com/jacomyal/sigma.js/issues) and proposing [pull requests](http://github.com/jacomyal/sigma.js/pulls). Make sure that tests and linting pass before submitting any pull request by running the command `grunt`.\n\nThe whole source code is validated by the [Google Closure Linter](https://developers.google.com/closure/utilities/) and [JSHint](http://www.jshint.com/), and the comments are written in [JSDoc](http://en.wikipedia.org/wiki/JSDoc) (tags description is available [here](https://developers.google.com/closure/compiler/docs/js-for-compiler)).\n"
  },
  {
    "path": "www/sigma/sigma.canvas.edges.curvedArrow.js",
    "content": ";(function() {\n  'use strict';\n\n  sigma.utils.pkg('sigma.canvas.edges');\n\n  /**\n   * This edge renderer will display edges as curves with arrow heading.\n   *\n   * @param  {object}                   edge         The edge object.\n   * @param  {object}                   source node  The edge source node.\n   * @param  {object}                   target node  The edge target node.\n   * @param  {CanvasRenderingContext2D} context      The canvas context.\n   * @param  {configurable}             settings     The settings function.\n   */\n  sigma.canvas.edges.curvedArrow =\n    function(edge, source, target, context, settings) {\n    var color = edge.color,\n        prefix = settings('prefix') || '',\n        edgeColor = settings('edgeColor'),\n        defaultNodeColor = settings('defaultNodeColor'),\n        defaultEdgeColor = settings('defaultEdgeColor'),\n        cp = {},\n        size = edge[prefix + 'size'] || 1,\n        tSize = target[prefix + 'size'],\n        sX = source[prefix + 'x'],\n        sY = source[prefix + 'y'],\n        tX = target[prefix + 'x'],\n        tY = target[prefix + 'y'],\n        aSize = Math.max(size * 2.5, settings('minArrowSize')),\n        d,\n        aX,\n        aY,\n        vX,\n        vY;\n\n    cp = (source.id === target.id) ?\n      sigma.utils.getSelfLoopControlPoints(sX, sY, tSize) :\n      sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY);\n\n    if (source.id === target.id) {\n      d = Math.sqrt(Math.pow(tX - cp.x1, 2) + Math.pow(tY - cp.y1, 2));\n      aX = cp.x1 + (tX - cp.x1) * (d - aSize - tSize) / d;\n      aY = cp.y1 + (tY - cp.y1) * (d - aSize - tSize) / d;\n      vX = (tX - cp.x1) * aSize / d;\n      vY = (tY - cp.y1) * aSize / d;\n    }\n    else {\n      d = Math.sqrt(Math.pow(tX - cp.x, 2) + Math.pow(tY - cp.y, 2));\n      aX = cp.x + (tX - cp.x) * (d - aSize - tSize) / d;\n      aY = cp.y + (tY - cp.y) * (d - aSize - tSize) / d;\n      vX = (tX - cp.x) * aSize / d;\n      vY = (tY - cp.y) * aSize / d;\n    }\n\n    if (!color)\n      switch (edgeColor) {\n        case 'source':\n          color = source.color || defaultNodeColor;\n          break;\n        case 'target':\n          color = target.color || defaultNodeColor;\n          break;\n        default:\n          color = defaultEdgeColor;\n          break;\n      }\n\n    context.strokeStyle = color;\n    context.lineWidth = size;\n    context.beginPath();\n    context.moveTo(sX, sY);\n    if (source.id === target.id) {\n      context.bezierCurveTo(cp.x2, cp.y2, cp.x1, cp.y1, aX, aY);\n    } else {\n      context.quadraticCurveTo(cp.x, cp.y, aX, aY);\n    }\n    context.stroke();\n\n    context.fillStyle = color;\n    context.beginPath();\n    context.moveTo(aX + vX, aY + vY);\n    context.lineTo(aX + vY * 0.6, aY - vX * 0.6);\n    context.lineTo(aX - vY * 0.6, aY + vX * 0.6);\n    context.lineTo(aX + vX, aY + vY);\n    context.closePath();\n    context.fill();\n  };\n})();\n"
  },
  {
    "path": "www/sigma/sigma.canvas.labels.def.js",
    "content": ";(function(undefined) {\n  'use strict';\n\n  if (typeof sigma === 'undefined')\n    throw 'sigma is not declared';\n\n  // Initialize packages:\n  sigma.utils.pkg('sigma.canvas.labels');\n\n  /**\n   * This label renderer will just display the label on the right of the node.\n   *\n   * @param  {object}                   node     The node object.\n   * @param  {CanvasRenderingContext2D} context  The canvas context.\n   * @param  {configurable}             settings The settings function.\n   */\n  sigma.canvas.labels.def = function(node, context, settings) {\n    var fontSize,\n        prefix = settings('prefix') || '',\n        size = node[prefix + 'size'];\n\n    // Hardcoded: always show labels\n    // if (size < settings('labelThreshold'))\n    //   return;\n\n    if (!node.label || typeof node.label !== 'string')\n      return;\n\n    // Hardcoded: Make labels proportionally sized\n    fontSize = settings('labelSizeRatio') * size;\n\n    // fontSize = (settings('labelSize') === 'fixed') ?\n    //   settings('defaultLabelSize') :\n    //   settings('labelSizeRatio') * size;\n\n    context.font = (settings('fontStyle') ? settings('fontStyle') + ' ' : '') +\n      fontSize + 'px ' + settings('font');\n    context.fillStyle = (settings('labelColor') === 'node') ?\n      (node.color || settings('defaultNodeColor')) :\n      settings('defaultLabelColor');\n\n    context.fillText(\n      node.label,\n      Math.round(node[prefix + 'x'] + size + 3),\n      Math.round(node[prefix + 'y'] + fontSize / 3)\n    );\n  };\n}).call(this);\n"
  },
  {
    "path": "www/style.css",
    "content": "body {\n\tcolor: #333;\n\tfont-size: 14px;\n\tfont-family: Lato, sans-serif;\n}\n\n#graph-container {\n\ttop: 0;\n\tbottom: 0;\n\tleft: 0;\n\tright: 0;\n\tposition: absolute;\n\tbackground: rgb(249, 247, 237); /* A light beige */\n}\n\n#control-pane {\n\ttop: 10px;\n\tright: 10px;\n\tposition: absolute;\n\twidth: 230px;\n\tbackground-color: rgb(249, 247, 237);\n\tbox-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);\n}\n\n#control-pane > div {\n\tmargin: 10px;\n\toverflow-x: auto;\n}\n\n.line {\n\tclear: both;\n\tdisplay: block;\n\twidth: 100%;\n\tmargin: 0;\n\tpadding: 12px 0 0 0;\n\tborder-bottom: 1px solid rgba(0, 0, 0, 0.1);\n\tbackground: transparent;\n}\n\nh2, h3 {\n\tpadding: 0;\n\ttext-transform: uppercase;\n}\n\nh2.underline {\n\tbackground: #f4f0e4;\n\tmargin: 0;\n\tborder-radius: 2px;\n\tpadding: 8px 12px;\n\tfont-weight: 700;\n}\n\ninput[type=range] {\n\twidth: 160px;\n}\n"
  }
]