From 1b36ade19059f60521a623d8a9289707a5b8d79e Mon Sep 17 00:00:00 2001 From: Ronan Klyne Date: Mon, 23 Mar 2020 10:10:35 +0000 Subject: [PATCH] Add a module import view. It's visible in the test app at `/deps` --- Pipfile | 17 +++++++ Pipfile.lock | 98 +++++++++++++++++++++++++++++++++++++++++ schema_graph/modules.py | 73 ++++++++++++++++++++++++++++++ schema_graph/views.py | 26 +++++++++++ setup.py | 2 +- tests/urls.py | 16 +++++-- 6 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 schema_graph/modules.py diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..aad716f --- /dev/null +++ b/Pipfile @@ -0,0 +1,17 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +django = "==1.8" +django-environ = "*" +pywatchman = "*" + +[packages] +attrs = "*" +pydeps = "*" +django = "~=1.8" + +[requires] +python_version = "2.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..18c32bc --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,98 @@ +{ + "_meta": { + "hash": { + "sha256": "4d7d978a7ed12bc8673dbd105d2de581f0e86a9e82b6db22e98a113a0ded9541" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "2.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "index": "pypi", + "version": "==19.3.0" + }, + "django": { + "hashes": [ + "sha256:014e3392058d94f40569206a24523ce254d55ad2f9f46c6550b0fe2e4f94cf3f", + "sha256:4200aefb6678019a0acf0005cd14cfce3a5e6b9b90d06145fcdd2e474ad4329c" + ], + "index": "pypi", + "version": "==1.11.29" + }, + "enum34": { + "hashes": [ + "sha256:a98a201d6de3f2ab3db284e70a33b0f896fbf35f8086594e8c9e74b909058d53", + "sha256:c3858660960c984d6ab0ebad691265180da2b43f07e061c0f8dca9ef3cffd328", + "sha256:cce6a7477ed816bd2542d03d53db9f0db935dd013b70f336a95c73979289f248" + ], + "markers": "python_version < '3.4'", + "version": "==1.1.10" + }, + "functools32": { + "hashes": [ + "sha256:89d824aa6c358c421a234d7f9ee0bd75933a67c29588ce50aaa3acdf4d403fa0", + "sha256:f6253dfbe0538ad2e387bd8fdfd9293c925d63553f5813c4e587745416501e6d" + ], + "version": "==3.2.3.post2" + }, + "pydeps": { + "hashes": [ + "sha256:59d6d0f5a2b8ebdb11caf0439dd36ad7b918de7c8c338ba9d526b8fd700a346d", + "sha256:ba9b8c7d72cb4dfd3f4dd6b8a250c240d15824850a415fd428f2660ed371361f" + ], + "index": "pypi", + "version": "==1.9.0" + }, + "pytz": { + "hashes": [ + "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", + "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" + ], + "version": "==2019.3" + }, + "stdlib-list": { + "hashes": [ + "sha256:133cc99104f5a4e1604dc88ebb393529bd4c2b99ae7e10d46c0b596f3c67c3f0" + ], + "version": "==0.6.0" + } + }, + "develop": { + "django": { + "hashes": [ + "sha256:014e3392058d94f40569206a24523ce254d55ad2f9f46c6550b0fe2e4f94cf3f", + "sha256:4200aefb6678019a0acf0005cd14cfce3a5e6b9b90d06145fcdd2e474ad4329c" + ], + "index": "pypi", + "version": "==1.11.29" + }, + "django-environ": { + "hashes": [ + "sha256:6c9d87660142608f63ec7d5ce5564c49b603ea8ff25da595fd6098f6dc82afde", + "sha256:c57b3c11ec1f319d9474e3e5a79134f40174b17c7cc024bbb2fad84646b120c4" + ], + "index": "pypi", + "version": "==0.4.5" + }, + "pywatchman": { + "hashes": [ + "sha256:d0047eb275deafb0011eda0a1a815fbd9742478c3d2b5ad6956d300e447dc2f9" + ], + "index": "pypi", + "version": "==1.4.1" + } + } +} diff --git a/schema_graph/modules.py b/schema_graph/modules.py new file mode 100644 index 0000000..0c62a63 --- /dev/null +++ b/schema_graph/modules.py @@ -0,0 +1,73 @@ +import logging + +from django.conf import settings +from pydeps import cli, py2depgraph, target + +from .schema import Schema + +log = logging.getLogger(__name__) + + +def _get_label(name): + return _split_name(name)[0] + + +def _split_name(name): + result = name.split('.', 1) + if len(result) == 1: + return [result[0]] * 2 + return result + + +def get_modules_as_schema(base_dir): + cli.verbose = lambda *args: log.debug(*args) + MAX_BACON = 2 # default: 2**65 + EXCLUDE_MODULES = [ + 'django' + ] + + excludes = [] + for name in EXCLUDE_MODULES: + # excludes.append(name) + excludes.append(name+'.*') + kwargs = dict( + T='svg', config=None, debug=False, display=None, exclude=excludes, + exclude_exact=[], + externals=False, format='svg', max_bacon=MAX_BACON, no_config=True, + nodot=False, + noise_level=2**65, noshow=True, output=None, pylib=False, pylib_all=False, + show=False, show_cycles=False, show_deps=False, show_dot=False, + show_raw_deps=False, verbose=0, include_missing=True, start_color=0 + ) + graph = py2depgraph.py2dep(target.Target(base_dir), **kwargs) + + nodes = set() + imports = set() + for a, b in graph: + # b imports a + nodes.add(a.name) + nodes.add(b.name) + new_import = [tuple(_split_name(b.name)), tuple(_split_name(a.name))] + log.debug("adding import %s", new_import) + imports.add(tuple(new_import)) + + models = {} + for name in nodes: + label, short_name = _split_name(name) + models.setdefault(label, set()).add(short_name) + + for app_group in models.keys(): + models[app_group] = sorted(models[app_group]) + + return Schema( + # Vertices + abstract_models={}, + models=models, + proxies=[], + # Edges + foreign_keys=sorted(imports), + inheritance=[], + many_to_manys=[], + one_to_ones=[], + ) + diff --git a/schema_graph/views.py b/schema_graph/views.py index cf1af67..bc213f8 100644 --- a/schema_graph/views.py +++ b/schema_graph/views.py @@ -6,6 +6,7 @@ from django.views.generic import TemplateView from schema_graph.schema import get_schema +from schema_graph.modules import get_modules_as_schema def debug_required(view_function): @@ -39,3 +40,28 @@ def get_context_data(self, **kwargs): } ) return super(Schema, self).get_context_data(**kwargs) + + +class Modules(TemplateView): + template_name = "schema_graph/schema.html" + base_dir = None + + # Not using `name="dispatch"` here so that we can support Django 1.8. + @method_decorator(debug_required) + def dispatch(self, request, *args, **kwargs): + return super(Modules, self).dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + schema = get_modules_as_schema(base_dir=self.base_dir) + kwargs.update( + { + "abstract_models": json.dumps(schema.abstract_models), + "models": json.dumps(schema.models), + "foreign_keys": json.dumps(schema.foreign_keys), + "many_to_manys": json.dumps(schema.many_to_manys), + "one_to_ones": json.dumps(schema.one_to_ones), + "inheritance": json.dumps(schema.inheritance), + "proxies": json.dumps(schema.proxies), + } + ) + return super(Modules, self).get_context_data(**kwargs) diff --git a/setup.py b/setup.py index 1decab7..6ca123a 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,7 @@ def run(self): cmdclass={"verify": VerifyVersionCommand}, description="An interactive graph of your Django model structure.", include_package_data=True, - install_requires=["attrs"], + install_requires=["attrs", "pydeps"], license="MIT", long_description=long_description, long_description_content_type="text/markdown", diff --git a/tests/urls.py b/tests/urls.py index 88938f5..e210c9e 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,13 +1,23 @@ -from schema_graph.views import Schema +import os +from schema_graph.views import Modules, Schema + + +base_dir = os.path.dirname(__file__) try: # Django 2+: from django.urls import path - urlpatterns = [path("", Schema.as_view())] + urlpatterns = [ + path("", Schema.as_view()), + path("deps", Schema.as_view()), + ] except ImportError: # Django < 2: from django.conf.urls import url - urlpatterns = [url(r"^$", Schema.as_view())] + urlpatterns = [ + url(r"^$", Schema.as_view()), + url(r"^deps$", Modules.as_view(base_dir=base_dir)), + ]