tool.dev.export_schemata()   F
last analyzed

Complexity

Conditions 17

Size

Total Lines 120
Code Lines 75

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 17
eloc 75
nop 6
dl 0
loc 120
rs 1.8
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like tool.dev.export_schemata() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#!/usr/bin/env python
2
# -*- coding: UTF-8 -*-
3
4
# Isomer - The distributed application framework
5
# ==============================================
6
# Copyright (C) 2011-2020 Heiko 'riot' Weinen <[email protected]> and others.
7
#
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU Affero General Public License as published by
10
# the Free Software Foundation, either version 3 of the License, or
11
# (at your option) any later version.
12
#
13
# This program is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
# GNU Affero General Public License for more details.
17
#
18
# You should have received a copy of the GNU Affero General Public License
19
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
21
22
"""
23
24
Module: Dev
25
===========
26
27
A collection of developer support tools.
28
29
"""
30
31
import time
32
import pkg_resources
33
import click
34
import os
35
import shutil
36
import json
37
38
from pprint import pprint
39
from click_didyoumean import DYMGroup
40
from collections import OrderedDict, namedtuple
41
from operator import attrgetter
42
43
from isomer.tool import log, ask, run_process, finish
44
from isomer.tool.templates import write_template_file
45
from isomer.logger import debug, verbose, warn
46
from isomer.misc.std import std_table
47
from isomer.error import abort
48
from isomer.ui.store.inventory import populate_store, get_inventory
49
from isomer.ui.builder import get_components
50
from isomer.events.system import generate_asyncapi
51
52
paths = [
53
    "isomer",
54
    "isomer/{plugin_name}",
55
    "isomer-frontend/{plugin_name}/scripts/controllers",
56
    "isomer-frontend/{plugin_name}/views",
57
]
58
59
templates = {
60
    "setup_file": ("setup.py.template", "setup.py"),
61
    "package_file": ("package.json.template", "package.json"),
62
    "component": ("component.py.template", "isomer/{plugin_name}/{plugin_name}.py"),
63
    "module_init": ("init.py.template", "isomer/__init__.py"),
64
    "package_init": ("init.py.template", "isomer/{plugin_name}/__init__.py"),
65
    "schemata": ("schemata.py.template", "isomer/{plugin_name}/schemata.py"),
66
    "controller": (
67
        "controller.js.template",
68
        "isomer-frontend/{plugin_name}/scripts/controllers/{" "plugin_name}.js",
69
    ),
70
    "view": (
71
        "view.html.template",
72
        "isomer-frontend/{plugin_name}/views/{plugin_name}.html",
73
    ),
74
}
75
76
questions = OrderedDict(
77
    {
78
        "plugin_name": "plugin",
79
        "author_name": u"author",
80
        "author_email": u"[email protected]",
81
        "description": u"Description",
82
        "long_description": u"Very long description, use \\n to get multilines.",
83
        "version": "0.0.1",
84
        "github_url": "isomeric/example",
85
        "license": "GPLv3",
86
        "keywords": "Isomer example plugin",
87
    }
88
)
89
90
info_header = """The manage command guides you through setting up a new isomer
91
package.
92
It provides basic setup. If you need dependencies or have other special
93
needs, edit the resulting files by hand.
94
95
You can press Ctrl-C any time to cancel this process.
96
97
See iso create-module --help for more details.
98
"""
99
100
101
def _augment_info(info):
102
    """Fill out the template information"""
103
104
    info["description_header"] = "=" * len(info["description"])
105
    info["component_name"] = info["plugin_name"].capitalize()
106
    info["year"] = time.localtime().tm_year
107
    info["license_longtext"] = ""
108
109
    info["keyword_list"] = u""
110
    for keyword in info["keywords"].split(" "):
111
        print(keyword)
112
        info["keyword_list"] += u"'" + str(keyword) + u"', "
113
    print(info["keyword_list"])
114
    if len(info["keyword_list"]) > 0:
115
        # strip last comma
116
        info["keyword_list"] = info["keyword_list"][:-2]
117
118
    return info
119
120
121
def _construct_module(info, target):
122
    """Build a module from templates and user supplied information"""
123
124
    for path in paths:
125
        real_path = os.path.abspath(os.path.join(target, path.format(**info)))
126
        log("Making directory '%s'" % real_path)
127
        os.makedirs(real_path)
128
129
    # pprint(info)
130
    for item in templates.values():
131
        source = os.path.join("dev/templates", item[0])
132
        filename = os.path.abspath(os.path.join(target, item[1].format(**info)))
133
        log("Creating file from template '%s'" % filename, emitter="MANAGE")
134
        write_template_file(source, filename, info)
135
136
137
def _ask_questionnaire():
138
    """Asks questions to fill out a Isomer plugin template"""
139
140
    answers = {}
141
    print(info_header)
142
    pprint(questions.items())
143
144
    for question, default in questions.items():
145
        response = ask(question, default, str(type(default)), show_hint=True)
146
        if type(default) == bytes and type(response) != str:
147
            response = response.decode("utf-8")
148
        answers[question] = response
149
150
    return answers
151
152
153
@click.group(
154
    cls=DYMGroup,
155
    short_help="Developer support operations"
156
)
157
def dev():
158
    """[GROUP] Developer support operations"""
159
160
161
@dev.command(short_help="generate async-api definition")
162
@click.option(
163
    "--filename",
164
    "-f",
165
    help="Filename to write output",
166
    default="",
167
    type=click.types.Path(exists=False),
168
)
169
@click.pass_context
170
def generate_api(ctx, filename):
171
    """Generate and output isomer async api definition"""
172
173
    _ = get_components("")
174
175
    api = generate_asyncapi()
176
177
    if filename == "":
178
        print(api)
179
    else:
180
        with open(filename, "w") as f:
181
            json.dump(api, f, indent=4)
182
183
184
@dev.command(short_help="list known events")
185
def events():
186
    """List all known authorized and anonymous events"""
187
188
    from isomer.events.system import (
189
        get_anonymous_events,
190
        get_user_events,
191
        populate_user_events,
192
    )
193
194
    populate_user_events()
195
196
    event_list = {**get_user_events(), **get_anonymous_events()}
197
198
    log("Events:\n", event_list, pretty=True)
199
200
201
@dev.command(short_help="export schemata")
202
@click.option("--output-path", "-o", help="output path", type=click.types.Path())
203
@click.option(
204
    "--output-format",
205
    "--format"
206
    "-f",
207
    help="Specify format",
208
    default="jsonschema",
209
    type=click.types.Choice(["jsonschema", "typescript"]),
210
)
211
@click.option(
212
    "--no-entity-mode",
213
    "-n",
214
    help="Do not use ngrx-auto-entity mode (removes key and does not uses classes)",
215
    default=False,
216
    is_flag=True,
217
)
218
@click.option(
219
    "--include-meta",
220
    "-m",
221
    is_flag=True,
222
    default=False,
223
    help="Include meta properties like permissions",
224
)
225
@click.argument("schemata", nargs=-1)
226
@click.pass_context
227
def export_schemata(ctx, output_path, output_format, no_entity_mode, include_meta, schemata):
228
    """Utility function for exporting known schemata to various formats
229
230
    Exporting to typescript requires the "json-schema-to-typescript" tool.
231
    You can install it via:
232
233
        npm -g install json-schema-to-typescript
234
    """
235
236
    # TODO: This one is rather ugly to edit, should the need arise..
237
    banner = (
238
        "/* tslint:disable */\n/**\n* This file was automatically generated "
239
        "by Isomer's command line tool using:\n"
240
        " * 'iso dev export-schemata -f typescript' "
241
        "- using json-schema-to-typescript.\n"
242
        " * DO NOT MODIFY IT BY HAND. Instead, modify the source isomer object "
243
        "file,\n* and run the iso tool schemata exporter again, to regenerate this file.\n*/"
244
    )
245
246
    # TODO: This should be employable to automatically generate
247
    #  typescript definitions inside a modules frontend part as part
248
    #  of the development cycle.
249
250
    from isomer import database, schemastore
251
252
    database.initialize(ctx.obj["dbhost"], ctx.obj["dbname"], ignore_fail=True)
253
254
    if len(schemata) == 0:
255
        schemata = database.objectmodels.keys()
256
257
    if output_path is not None:
258
        stdout = False
259
        if not os.path.exists(output_path):
260
            abort("Output Path doesn't exist.")
261
    else:
262
        stdout = True
263
264
    for item in schemata:
265
        if item not in schemastore.schemastore:
266
            log("Schema not registered:", item, lvl=warn)
267
            continue
268
269
        schema = schemastore.schemastore[item]["schema"]
270
271
        if not include_meta:
272
            if "perms" in schema["properties"]:
273
                del schema["properties"]["perms"]
274
            if "roles_create" in schema:
275
                del schema["roles_create"]
276
            if "required" in schema:
277
                del schema["required"]
278
279
        if output_format == "jsonschema":
280
            log("Generating json schema of", item)
281
282
            if stdout:
283
                print(json.dumps(schema, indent=4))
284
            else:
285
                with open(os.path.join(output_path, item + ".json"), "w") as f:
286
                    json.dump(schema, f, indent=4)
287
288
        elif output_format == "typescript":
289
            log("Generating typescript annotations of", item)
290
291
            # TODO: Fix typing issues here and esp. in run_process
292
            success, result = run_process(
293
                output_path,
294
                [
295
                    "json2ts",
296
                    "--bannerComment",
297
                    banner,
298
                ],
299
                stdin=json.dumps(schema).encode("utf-8"),
300
            )
301
            typescript = result.output.decode("utf-8")
302
303
            if no_entity_mode is False:
304
                typescript = (
305
                        "import { Entity, Key } from '@briebug/ngrx-auto-entity';\n"
306
                        + typescript
307
                )
308
                typescript = typescript.replace("uuid", "@Key uuid")
309
                typescript = typescript.replace(
310
                    "export interface",
311
                    "@Entity({modelName: '%s'})\n" "export class" % item,
312
                )
313
314
            if stdout:
315
                print(typescript)
316
            else:
317
                with open(os.path.join(output_path, item + ".ts"), "w") as f:
318
                    f.write(typescript)
319
320
    finish(ctx)
321
322
323
@dev.command(short_help="create starterkit module")
324
@click.option(
325
    "--clear-target",
326
    "--clear",
327
    help="Clears already existing target",
328
    default=False,
329
    is_flag=True,
330
)
331
@click.option(
332
    "--target",
333
    help="Create module in the given folder (uses ./ if omitted)",
334
    default=".",
335
    metavar="<folder>",
336
)
337
def create_module(clear_target, target):
338
    """Creates a new template Isomer plugin module"""
339
340
    if os.path.exists(target):
341
        if clear_target:
342
            shutil.rmtree(target)
343
        else:
344
            log("Target exists! Use --clear to delete it first.", emitter="MANAGE")
345
            abort(2)
346
347
    done = False
348
    info = None
349
350
    while not done:
351
        info = _ask_questionnaire()
352
        pprint(info)
353
        done = ask("Is the above correct", default="y", data_type="bool")
354
355
    augmented_info = _augment_info(info)
356
357
    log("Constructing module %(plugin_name)s" % info)
358
    _construct_module(augmented_info, target)
359
360
361
@dev.command(short_help="List setuptools installed component information")
362
@click.option(
363
    "--base",
364
    "-b",
365
    is_flag=True,
366
    default=False,
367
    help="Also list isomer-base (integrated) modules",
368
)
369
@click.option(
370
    "--sails",
371
    "-s",
372
    is_flag=True,
373
    default=False,
374
    help="Also list isomer-sails (integrated) modules",
375
)
376
@click.option(
377
    "--schemata",
378
    is_flag=True,
379
    default=False,
380
    help="Also list registered schemata"
381
)
382
@click.option(
383
    "--management",
384
    is_flag=True,
385
    default=False,
386
    help="Also list registered management commands"
387
)
388
@click.option(
389
    "--provisions",
390
    is_flag=True,
391
    default=False,
392
    help="Also list registered provisions"
393
)
394
@click.option(
395
    "--frontend-only",
396
    "-f",
397
    is_flag=True,
398
    default=False,
399
    help="Only list modules with a frontend",
400
)
401
@click.option(
402
    "--frontend-list",
403
    "-l",
404
    is_flag=True,
405
    default=False,
406
    help="List files in frontend per module",
407
)
408
@click.option(
409
    "--directory", "-d", is_flag=True, default=False, help="Show directory of module"
410
)
411
@click.option(
412
    "--sort-key",
413
    "-k",
414
    default="package",
415
    type=click.Choice(
416
        ["name", "package", "classname", "location", "frontend", "group"]),
417
)
418
@click.option(
419
    "--list-all",
420
    "--all",
421
    "-a",
422
    is_flag=True,
423
    default=False,
424
    help="List all registered entrypoints"
425
)
426
@click.option(
427
    "--filter-string",
428
    "--filter",
429
    "-f",
430
    default="",
431
    type=str,
432
    help="Filter table by string"
433
)
434
@click.option("--long", is_flag=True, default=False, help="Show full table")
435
def entrypoints(base, sails, schemata, management, provisions, frontend_only,
436
                frontend_list, directory, sort_key, list_all, filter_string, long):
437
    """Display list of entrypoints and diagnose module loading problems."""
438
439
    log("Showing entrypoints:")
440
441
    full_component = namedtuple(
442
        "Component", ["name", "package", "classname", "location", "frontend", "group"]
443
    )
444
    component = namedtuple("Component", ["name", "package", "frontend", "group"])
445
    results = []
446
447
    from pkg_resources import iter_entry_points
448
449
    entry_points = {
450
        "components": iter_entry_points(group="isomer.components", name=None)
451
    }
452
453
    if list_all:
454
        sails = base = schemata = management = provisions = True
455
456
    if sails:
457
        entry_points["sails"] = iter_entry_points(group="isomer.sails", name=None)
458
    if base:
459
        entry_points["base"] = iter_entry_points(group="isomer.base", name=None)
460
    if schemata:
461
        entry_points["schemata"] = iter_entry_points(group="isomer.schemata", name=None)
462
    if management:
463
        entry_points["management"] = iter_entry_points(group="isomer.management",
464
                                                       name=None)
465
    if provisions:
466
        entry_points["provisions"] = iter_entry_points(group="isomer.provisions",
467
                                                       name=None)
468
469
    log("Entrypoints:", entry_points, pretty=True, lvl=verbose)
470
    try:
471
        for key, iterator in entry_points.items():
472
            for entry_point in iterator:
473
                log("Entrypoint Group:", key, entry_point, pretty=True, lvl=debug)
474
475
                try:
476
                    name = entry_point.name
477
                    package = entry_point.dist.project_name
478
                    log("Package:", package, pretty=True, lvl=debug)
479
                    location = entry_point.dist.location
480
                    try:
481
                        loaded = entry_point.load()
482
                    except pkg_resources.DistributionNotFound as e:
483
                        log(
484
                            "Required distribution not found:",
485
                            e,
486
                            pretty=True,
487
                            exc=True,
488
                            lvl=warn,
489
                        )
490
                        continue
491
492
                    log(
493
                        "Entry point: ",
494
                        entry_point,
495
                        name,
496
                        entry_point.resolve(),
497
                        location,
498
                        lvl=debug,
499
                    )
500
501
                    log("Loaded: ", loaded, lvl=debug)
502
503
                    try:
504
                        pkg = pkg_resources.Requirement.parse(package)
505
                        frontend = pkg_resources.resource_listdir(pkg, "frontend")
506
                        log("Frontend resources found:", frontend, lvl=debug)
507
                    except Exception as e:
508
                        log(
509
                            "Exception during frontend resource lookup:",
510
                            e,
511
                            lvl=debug,
512
                            exc=True,
513
                        )
514
                        frontend = None
515
516
                    if frontend not in (None, []):
517
                        log("Contains frontend parts", lvl=debug)
518
                        if not frontend_list:
519
                            frontend = "[X]"
520
                    else:
521
                        frontend = "[ ]"
522
523
                    if filter_string != "":
524
                        if filter_string not in name and filter_string not in package and filter_string not in location:
525
                            continue
526
527
                    if long:
528
                        if key in ("schemata", "provisions"):
529
                            classname = "[SCHEMA]"
530
                        else:
531
                            classname = repr(loaded).lstrip("<class '").rstrip("'>")
532
533
                        result = full_component(
534
                            frontend=frontend,
535
                            name=name,
536
                            package=package,
537
                            classname=classname,
538
                            location=location if directory else "use -d",
539
                            group=key,
540
                        )
541
                    else:
542
                        result = component(
543
                            frontend=frontend, name=name, package=package, group=key
544
                        )
545
546
                    if not frontend_only or frontend:
547
                        results.append(result)
548
                except ImportError as e:
549
                    log("Exception while iterating entrypoints:", e, type(e), lvl=warn,
550
                        exc=True)
551
    except ModuleNotFoundError as e:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable ModuleNotFoundError does not seem to be defined.
Loading history...
552
        log("Module could not be loaded:", e, exc=True)
553
554
    results = sorted(results, key=attrgetter(sort_key))
555
556
    table = std_table(results)
557
    log("\n%s" % table)
558
559
560
@dev.command(short_help="Grab and show software store inventory")
561
@click.option(
562
    "--source",
563
    help="Specify a different source than official Isomer",
564
    default="https://store.isomer.eu/simple",
565
    metavar="<url>",
566
)
567
def store_inventory(source):
568
    """List available pacakages"""
569
570
    store = populate_store(source)
571
572
    log(store, pretty=True)
573
574
575
@dev.command(short_help="Show local inventory")
576
@click.pass_context
577
def local_inventory(ctx):
578
    """List installed pacakages"""
579
580
    inventory = get_inventory(ctx)
581
582
    log(inventory, pretty=True)
583