From 0168e5207c0a477dbc5ce207d2740d55cec41584 Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Thu, 12 Nov 2020 10:04:50 -0500 Subject: [PATCH 01/20] make label key more consistent with elk schema --- py_src/ipyelk/nx/transformer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/py_src/ipyelk/nx/transformer.py b/py_src/ipyelk/nx/transformer.py index a2b262a3..fa0570f9 100644 --- a/py_src/ipyelk/nx/transformer.py +++ b/py_src/ipyelk/nx/transformer.py @@ -50,7 +50,7 @@ class XELK(ElkTransformer): css_classes = T.Dict() port_scale = T.Int(default_value=5) - label_key = T.Unicode(default_value="label") + label_key = T.Unicode(default_value="labels") port_key = T.Unicode(default_value="ports") text_sizer: ElkTextSizer = T.Instance(ElkTextSizer, kw={}, allow_none=True) @@ -133,6 +133,7 @@ async def transform(self) -> ElkNode: :return: Root Elk node :rtype: ElkNode """ + # TODO decide behavior for nodes that exist in the tree but not g g, tree = self.source self.clear_cached() ports: PortMap = self._ports @@ -342,7 +343,7 @@ async def make_edge( properties = dict(cssClasses=" ".join(styles)) labels = [] - for i, label in enumerate(edge.data.get("labels", [])): + for i, label in enumerate(edge.data.get(self.label_key, [])): layout_options = self.get_layout( edge.owner, ElkLabel ) # TODO add edgelabel type? From 9fb19059af84928fd36d8fcb00a01a445750b1dd Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Sat, 14 Nov 2020 07:57:49 -0500 Subject: [PATCH 02/20] add Direction layout option --- .../ipyelk/diagram/layout_options/__init__.py | 2 ++ .../diagram/layout_options/edge_options.py | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/py_src/ipyelk/diagram/layout_options/__init__.py b/py_src/ipyelk/diagram/layout_options/__init__.py index 6a4949f0..36a8a171 100644 --- a/py_src/ipyelk/diagram/layout_options/__init__.py +++ b/py_src/ipyelk/diagram/layout_options/__init__.py @@ -5,6 +5,7 @@ # Distributed under the terms of the Modified BSD License. from .edge_options import ( + Direction, EadesRepulsion, EdgeCenterLabelPlacementStrategy, EdgeEdgeLayerSpacing, @@ -57,6 +58,7 @@ "CommentCommentSpacing", "CommentNodeSpacing", "ComponentsSpacing", + "Direction", "EadesRepulsion", "EdgeCenterLabelPlacementStrategy" "EdgeEdgeLayerSpacing", "EdgeCenterLabelPlacementStrategy", diff --git a/py_src/ipyelk/diagram/layout_options/edge_options.py b/py_src/ipyelk/diagram/layout_options/edge_options.py index b73e4570..107fa3ff 100644 --- a/py_src/ipyelk/diagram/layout_options/edge_options.py +++ b/py_src/ipyelk/diagram/layout_options/edge_options.py @@ -52,6 +52,14 @@ "Splines": "SPLINES", } +DIRECTION_OPTIONS = { + "Undefined": "UNDEFINED", + "Right": "RIGHT", + "Left": "LEFT", + "Down": "DOWN", + "Up": "UP", +} + class InlineEdgeLabels(LayoutOptionWidget): """If true, an edge label is placed directly on its edge. May only apply to @@ -395,3 +403,23 @@ def _ui(self) -> List[W.Widget]: @T.observe("merge") def _update_value(self, change: T.Bunch = None): self.value = "true" if self.merge else "false" + + +class Direction(LayoutOptionWidget): + """Overall direction of edges: horizontal (right / left) or vertical (down / up). + + https://www.eclipse.org/elk/reference/options/org-eclipse-elk-direction.html + """ + identifier = "org.eclipse.elk.direction" + metadata_provider = "core.options.CoreOptions" + applies_to = ["parents"] + + value = T.Enum( + values=list(DIRECTION_OPTIONS.values()), default_value="UNDEFINED" + ) + + def _ui(self) -> List[W.Widget]: + dropdown = W.Dropdown(options=list(DIRECTION_OPTIONS.items())) + T.link((self, "value"), (dropdown, "value")) + + return [dropdown] From 7a7df29c01ec4217b3a288992d0aa747c117f06e Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Mon, 16 Nov 2020 09:05:42 -0500 Subject: [PATCH 03/20] toward transformer restructuring --- py_src/ipyelk/diagram/elk_text_sizer.py | 25 ++++- py_src/ipyelk/nx/transformer.py | 134 +++++++++--------------- py_src/ipyelk/transform.py | 132 +++++------------------ 3 files changed, 97 insertions(+), 194 deletions(-) diff --git a/py_src/ipyelk/diagram/elk_text_sizer.py b/py_src/ipyelk/diagram/elk_text_sizer.py index c60f6876..37074c67 100644 --- a/py_src/ipyelk/diagram/elk_text_sizer.py +++ b/py_src/ipyelk/diagram/elk_text_sizer.py @@ -5,7 +5,7 @@ import asyncio import uuid from dataclasses import dataclass -from typing import Dict, List, Union +from typing import Dict, List, Optional, Union import traitlets as T from async_lru import alru_cache @@ -142,3 +142,26 @@ async def _process_responses(self): ) ) self._response_queue.task_done() + + +async def size_labels(text_sizer: Optional[ElkTextSizer], labels: List[ElkLabel]): + """Run a list of ElkLabels through to the TextSizer measurer + + :param labels: [description] + :type labels: List[ElkLabel] + :return: Updated Labels + """ + if text_sizer: + sizes = await text_sizer.measure(tuple(labels)) + else: + sizes = [ + TextSize( + width=10 * len(label.text), + height=10, # TODO add height default + ) + for label in labels + ] + + for size, label in zip(sizes, labels): + label.width = size.width + label.height = size.height diff --git a/py_src/ipyelk/nx/transformer.py b/py_src/ipyelk/nx/transformer.py index 8ea9d490..87844475 100644 --- a/py_src/ipyelk/nx/transformer.py +++ b/py_src/ipyelk/nx/transformer.py @@ -18,7 +18,7 @@ ElkPort, ElkRoot, ) -from ..diagram.elk_text_sizer import ElkTextSizer, TextSize +from ..diagram.elk_text_sizer import ElkTextSizer, size_labels from ..transform import ElkTransformer from .nx import ( Edge, @@ -41,10 +41,6 @@ class XELK(ElkTransformer): _hidden_edges: Optional[EdgeMap] = None _visible_edges: Optional[EdgeMap] = None - # internal map between output Elk Elements and incoming items - _elk_to_item: Dict[str, Hashable] = None - _item_to_elk: Dict[Hashable, str] = None - source = T.Tuple(T.Instance(nx.Graph), T.Instance(nx.DiGraph, allow_none=True)) layouts = T.Dict() # keys: networkx nodes {ElkElements: {layout options}} css_classes = T.Dict() @@ -144,8 +140,13 @@ async def transform(self) -> ElkNode: n for n in g.nodes() if not is_hidden(tree, n, self.HIDDEN_ATTR) ] + # make elknodes then connect their hierarchy elknodes = {node: await self.make_elknode(node) for node in visible_nodes} - top = self.build_hierarchy(elknodes) # top level node + elknodes[ElkRoot] = top = ElkNode( + id=self.ELK_ROOT_ID, + children=self.build_hierarchy(elknodes), + layoutOptions=self.get_layout(ElkRoot, "parents"), + ) # TODO flag to control if slack ports and edges should be hoisted to # closest visible ancestors @@ -174,7 +175,7 @@ async def transform(self) -> ElkNode: # self.register(label, port) # bulk calculate label sizes - await self.size_labels(self.collect_labels(elknodes)) + await size_labels(self.text_sizer, self.collect_labels(elknodes)) # link children to parents self._nodes = elknodes @@ -182,46 +183,6 @@ async def transform(self) -> ElkNode: self.register(elk_node, node) return top - def build_hierarchy(self, elknodes: NodeMap) -> ElkNode: - """The Elk JSON is hierarchical. This method iterates through the build - elknodes and links children to parents if the incoming source includes a - hierarcichal networkx diagraph tree. - - :param elknodes: mapping of networkx nodes to their elknode representations - :type elknodes: NodeMap - :return: The root elknode - :rtype: ElkNode - """ - g, tree = self.source - attr = self.HIDDEN_ATTR - if tree: - # roots of the tree - roots = [n for n, d in tree.in_degree() if d == 0] - for n, elknode in elknodes.items(): - if n in tree: - elknode.children = [ - elknodes[c] - for c in tree.neighbors(n) - if not is_hidden(tree, c, attr) - ] - else: - # nodes that are not in the tree - roots.append(n) - else: - # only flat graph provided - roots = [] - for n, elknode in elknodes.items(): - if not is_hidden(tree, n, attr): - roots.append(n) - - top = ElkNode( - id=self.ELK_ROOT_ID, - children=[elknodes[n] for n in roots], - layoutOptions=self.get_layout(ElkRoot, "parents"), - ) - elknodes[ElkRoot] = top # using `None` to represent root elknode - return top # returns top level node - async def make_elknode(self, node) -> ElkNode: layout = self.get_layout(node, ElkNode) @@ -396,8 +357,6 @@ async def make_edge( if isinstance(label, str): label = ElkLabel(id=f"{edge.owner}_label_{i}_{label}", text=label) label.layoutOptions = merge(label.layoutOptions, layout_options) - # TODO size the labels in bulk - await self.size_labels([label]) labels.append(label) for label in labels: self.register(label, edge) @@ -448,43 +407,6 @@ async def make_port( self._ports[port_id] = port return port - def register(self, element: ElkGraphElement, item: Hashable) -> str: - """Register Elk Element as a way to find the particular item. - - This method is used in the `lookup` method for dereferencing the elk id. - - :param element: [description] - :type element: ElkGraphElement - :param item: [description] - :type item: Hashable - """ - - self._elk_to_item[element.id] = item - self._item_to_elk[item] = element.id - return element - - async def size_labels(self, labels: List[ElkLabel]): - """Run a list of ElkLabels through to the TextSizer measurer - - :param labels: [description] - :type labels: List[ElkLabel] - :return: Updated Labels - """ - if self.text_sizer is not None: - sizes = await self.text_sizer.measure(tuple(labels)) - else: - sizes = [ - TextSize( - width=10 * len(label.text), - height=10, # TODO add height default - ) - for label in labels - ] - - for size, label in zip(sizes, labels): - label.width = size.width - label.height = size.height - def get_node_data(self, node: Hashable) -> Dict: g, tree = self.source return g.nodes.get(node, {}) @@ -747,3 +669,43 @@ def listed(values: Optional[List]) -> List: if values is None: return [] return values + + +def build_hierarchy( + g: nx.Graph, tree: nx.DiGraph, elknodes: NodeMap, HIDDEN_ATTR: str +) -> List[ElkNode]: + """The Elk JSON is hierarchical. This method iterates through the build + elknodes and links children to parents if the incoming source includes a + hierarcichal networkx diagraph tree. + + :param g: [description] + :type g: nx.Graph + :param tree: [description] + :type tree: nx.DiGraph + :param elknodes: mapping of networkx nodes to their elknode representations + :type elknodes: NodeMap + :param HIDDEN_ATTR: [description] + :type HIDDEN_ATTR: str + :return: Top level ElkNodes to put as children under the ElkRoot + :rtype: List[ElkNode] + """ + if tree: + # roots of the tree + roots = [n for n, d in tree.in_degree() if d == 0] + for n, elknode in elknodes.items(): + if n in tree: + elknode.children = [ + elknodes[c] + for c in tree.neighbors(n) + if not is_hidden(tree, c, HIDDEN_ATTR) + ] + else: + # nodes that are not in the tree + roots.append(n) + else: + # only flat graph provided + roots = [] + for n, elknode in elknodes.items(): + if not is_hidden(tree, n, HIDDEN_ATTR): + roots.append(n) + return [elknodes[n] for n in roots] diff --git a/py_src/ipyelk/transform.py b/py_src/ipyelk/transform.py index 298c4d4e..e386acb3 100644 --- a/py_src/ipyelk/transform.py +++ b/py_src/ipyelk/transform.py @@ -1,15 +1,14 @@ # Copyright (c) 2020 Dane Freeman. # Distributed under the terms of the Modified BSD License. import asyncio -from typing import Dict, Hashable, Optional +from typing import Dict, Hashable, Optional, Union import ipywidgets as W import traitlets as T from .diagram import ElkDiagram, ElkLabel, ElkNode +from .diagram.elk_model import ElkGraphElement from .schema import ElkSchemaValidator -from .styled_widget import StyledWidget -from .toolbar import Toolbar from .trait_types import Schema @@ -22,6 +21,10 @@ class ElkTransformer(W.Widget): _version: str = "v1" _task: asyncio.Task = None + # internal map between output Elk Elements and incoming items + _elk_to_item: Dict[str, Hashable] = None + _item_to_elk: Dict[Hashable, str] = None + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -69,111 +72,26 @@ def connect(self, view: ElkDiagram) -> T.link: """Connect the output value of this transformer to a diagram""" return T.dlink((self, "value"), (view, "value")) + def register(self, element: Union[str, ElkGraphElement], item: Hashable): + """Register Elk Element as a way to find the particular item. -class Elk(W.VBox, StyledWidget): - """ An Elk diagramming widget """ + This method is used in the `lookup` method for dereferencing the elk id. - transformer: ElkTransformer = T.Instance(ElkTransformer) - diagram: ElkDiagram = T.Instance(ElkDiagram) - selected = T.Tuple() - hovered = T.Any(allow_none=True, default_value=None) - toolbar: Toolbar = T.Instance(Toolbar, kw={}) + :param element: ElkGraphElement or elk id to track + :type element: ElkGraphElement + :param item: [description] + :type item: Hashable + """ - _data_link: T.dlink = None + _id = element.id if isinstance(element, ElkGraphElement) else element - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._update_data_link() - self._update_children() - self.add_class("jp-ElkApp") - - def _set_arrows_opacity(self, value): - style = self.style - css_selector = " path.edge.arrow" - - arrow_style = style.get(css_selector, {}) - arrow_style["opacity"] = str(value) - - style[css_selector] = arrow_style - self.style = style.copy() - # TODO should not need to trigger update this way but the observer isn't firing - self._update_style() - - def hide_arrows(self): - self._set_arrows_opacity(0) - - def show_arrows(self): - self._set_arrows_opacity(1) - - @T.default("diagram") - def _default_diagram(self): - return ElkDiagram() - - @T.default("transformer") - def _default_transformer(self): - return ElkTransformer() - - @T.observe("diagram", "transformer") - def _update_data_link(self, *_): - if isinstance(self._data_link, T.link): - self._data_link.unlink() - self._data_link = None - if self.transformer and self.diagram: - self._data_link = T.dlink( - (self.transformer, "value"), - (self.diagram, "value"), - ) - - @T.observe("diagram") - def _update_children(self, change: T.Bunch = None): - self.children = [self.diagram, self.toolbar] - - if change: - # uninstall old observers - safely_unobserve(change.old, "selected") - safely_unobserve(change.old, "hovered") - - if self.diagram: # also change.new - self.diagram.observe(self._handle_diagram_selected, "selected") - self.diagram.observe(self._handle_diagram_hovered, "hovered") - - def _handle_diagram_selected(self, change: T.Bunch): - items = [] - if change.new: - items = [self.transformer.from_id(s) for s in change.new] - if items != self.selected: - self.selected = items - - def _handle_diagram_hovered(self, change: T.Bunch): - try: - _id = self.transformer.from_id(change.new) - except ValueError: # TODO introduce custom ipyelk exceptions - _id = None - if _id != self.hovered: - self.hovered = _id - - @T.observe("selected") - def _update_selected(self, change: T.Bunch): - if not self.diagram: - return - - # transform selected nodes into ids and test if new ids - ids = [self.transformer.to_id(s) for s in self.selected] - if self.diagram.selected != ids: - self.diagram.selected = ids - - @T.observe("hovered") - def _update_hover(self, change: T.Bunch): - if not self.diagram: - return - - # transform hovered nodes into elk id - self.diagram.hover = self.transformer.to_id(self.hovered) - - def refresh(self): - self.transformer.refresh() - - -def safely_unobserve(item, handler): - if hasattr(item, "unobserve"): - item.unobserve(handler=handler) + if _id in self._elk_to_item: + raise ElkDuplicateIDError(f"{_id} already exists in the registry") + self._elk_to_item[_id] = item + self._item_to_elk[item] = _id + + +class ElkDuplicateIDError(Exception): + """Elk Ids must be unique""" + + pass From 8e9099426be2d5ebe545049efc0946a36aa1dadb Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Mon, 30 Nov 2020 17:02:28 -0500 Subject: [PATCH 04/20] more robust hovering. fix visible lookup if node isn't in the tree --- py_src/ipyelk/app.py | 14 +++++++++----- py_src/ipyelk/diagram/elk_model.py | 3 +++ py_src/ipyelk/nx/transformer.py | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/py_src/ipyelk/app.py b/py_src/ipyelk/app.py index 8adcbfef..9aa73b70 100644 --- a/py_src/ipyelk/app.py +++ b/py_src/ipyelk/app.py @@ -4,7 +4,7 @@ import traitlets as T from .diagram import ElkDiagram -from .diagram.elk_model import ElkRoot +from .diagram.elk_model import ElkNullElement from .styled_widget import StyledWidget from .toolbar import Toolbar from .transform import ElkTransformer @@ -16,7 +16,7 @@ class Elk(W.VBox, StyledWidget): transformer: ElkTransformer = T.Instance(ElkTransformer) diagram: ElkDiagram = T.Instance(ElkDiagram) selected = T.Tuple() - hovered = T.Any(allow_none=True, default_value=ElkRoot) + hovered = T.Any(allow_none=True, default_value=ElkNullElement) toolbar: Toolbar = T.Instance(Toolbar, kw={}) _data_link: T.dlink = None @@ -88,7 +88,7 @@ def _handle_diagram_hovered(self, change: T.Bunch): try: _id = self.transformer.from_id(change.new) except ValueError: # TODO introduce custom ipyelk exceptions - _id = None + _id = ElkNullElement if _id != self.hovered: self.hovered = _id @@ -104,11 +104,15 @@ def _update_selected(self, change: T.Bunch): @T.observe("hovered") def _update_hover(self, change: T.Bunch): - if not self.diagram: + if not self.diagram or self.hovered is ElkNullElement: return # transform hovered nodes into elk id - self.diagram.hover = self.transformer.to_id(self.hovered) + try: + self.diagram.hover = self.transformer.to_id(self.hovered) + except ValueError: # TODO introduce custom ipyelk exceptions + # okay not to pass the hovered state to the diagram? + pass def refresh(self): self.transformer.refresh() diff --git a/py_src/ipyelk/diagram/elk_model.py b/py_src/ipyelk/diagram/elk_model.py index a7d15a43..f167a02c 100644 --- a/py_src/ipyelk/diagram/elk_model.py +++ b/py_src/ipyelk/diagram/elk_model.py @@ -12,6 +12,9 @@ # Sentinel Value for tracking the root node in the Elk JSON ElkRoot = namedtuple("ElkRootNode", [])() +# Sentinel Value for tracking hovered mark state +ElkNullElement = namedtuple("ElkNullElement", [])() + T = TypeVar("T") diff --git a/py_src/ipyelk/nx/transformer.py b/py_src/ipyelk/nx/transformer.py index 8ea9d490..8aea3588 100644 --- a/py_src/ipyelk/nx/transformer.py +++ b/py_src/ipyelk/nx/transformer.py @@ -659,7 +659,7 @@ def closest_visible(self, node: Hashable): attr = self.HIDDEN_ATTR g, tree = self.source if node not in tree: - return None + return node if not is_hidden(tree, node, attr): return node predecesors = list(tree.predecessors(node)) From a2a07d75bdb39fa4a0bdfd7a259a97c729be7a8f Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Tue, 1 Dec 2020 09:02:25 -0500 Subject: [PATCH 05/20] Don't need escape the text sizer due to setting the value on the dom element using textContent --- src/measure_text.ts | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/src/measure_text.ts b/src/measure_text.ts index f07fec1f..61585542 100644 --- a/src/measure_text.ts +++ b/src/measure_text.ts @@ -6,15 +6,6 @@ import { DOMWidgetModel, DOMWidgetView } from '@jupyter-widgets/base'; import { NAME, VERSION, ELK_CSS, ELK_DEBUG } from '.'; -const TAG_REGEX = /[&<>'"]/g; -const TAGS_TO_REPLACE = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": '"'' -}; - export class ELKTextSizerModel extends DOMWidgetModel { static model_name = 'ELKTextSizerModel'; @@ -67,7 +58,7 @@ export class ELKTextSizerModel extends DOMWidgetModel { } label.classList.add(...classes); - label.textContent = escape(text.value); + label.textContent = text.value; ELK_DEBUG && console.warn('ELK Text Label', label); return label; } @@ -100,7 +91,7 @@ export class ELKTextSizerModel extends DOMWidgetModel { event: 'measurement', measurements: this.read_sizes(content.texts, elements) }; - document.body.removeChild(el); + // document.body.removeChild(el); this.send(response, {}, []); }); } @@ -159,17 +150,6 @@ export class ELKTextSizerView extends DOMWidgetView { async render() {} } -function escapeReplacer(tag: string): string { - return TAGS_TO_REPLACE[tag] || tag; -} - -/** - * Simple function to escape text for html before adding to dom - */ -function escape(text: string) { - return text.replace(TAG_REGEX, escapeReplacer); -} - /** * SVG Required Namespaced Element */ From 09111914114ec1843e26b5e9db755ed3873408b7 Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Tue, 1 Dec 2020 16:05:57 -0500 Subject: [PATCH 06/20] adjust test sizer to respect custom css for elk labels --- examples/101_text_sizer.ipynb | 54 +++++++++++++++++++++++-- py_src/ipyelk/diagram/elk_text_sizer.py | 8 ++-- src/elkschema.ts | 16 ++++++-- src/measure_text.ts | 7 +++- 4 files changed, 72 insertions(+), 13 deletions(-) diff --git a/examples/101_text_sizer.ipynb b/examples/101_text_sizer.ipynb index ec585efa..aada4b79 100644 --- a/examples/101_text_sizer.ipynb +++ b/examples/101_text_sizer.ipynb @@ -74,6 +74,15 @@ "Can send several requests" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sizer.style" + ] + }, { "cell_type": "code", "execution_count": null, @@ -82,7 +91,9 @@ "source": [ "num = 10\n", "bulk_sizer = ipyelk.diagram.ElkTextSizer(\n", - " max_size=num,\n", + " max_size=num, style={\n", + " \" .sprotty .elklabel.larger-font\": {\"font-size\": \"20px\"},\n", + " }\n", ")" ] }, @@ -92,13 +103,13 @@ "metadata": {}, "outputs": [], "source": [ - "async def send_bulk(num):\n", + "async def send_bulk_text(num):\n", " texts = tuple(str(i) for i in range(num))\n", " await bulk_sizer.measure(texts)\n", " display(bulk_sizer.measure.cache_info())\n", "\n", "\n", - "asyncio.create_task(send_bulk(num))" + "asyncio.create_task(send_bulk_text(num))" ] }, { @@ -108,6 +119,15 @@ "after running expect to see 10 additional requests captured in the `bulk_sizer.futures`" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bulk_sizer.raw_css" + ] + }, { "cell_type": "code", "execution_count": null, @@ -117,6 +137,34 @@ "bulk_sizer.measure.cache_info()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Size labels with css classes applied" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def label(i):\n", + " return ipyelk.diagram.elk_model.ElkLabel(\n", + " id=str(i), text=str(i), properties={\"cssClasses\": \"larger-font\"}\n", + " )\n", + "\n", + "\n", + "async def send_bulk_label(num):\n", + " texts = tuple(label(i) for i in range(num))\n", + " await bulk_sizer.measure(texts)\n", + " display(bulk_sizer.measure.cache_info())\n", + "\n", + "\n", + "asyncio.create_task(send_bulk_label(num))" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/py_src/ipyelk/diagram/elk_text_sizer.py b/py_src/ipyelk/diagram/elk_text_sizer.py index c60f6876..401d1dbf 100644 --- a/py_src/ipyelk/diagram/elk_text_sizer.py +++ b/py_src/ipyelk/diagram/elk_text_sizer.py @@ -9,9 +9,9 @@ import traitlets as T from async_lru import alru_cache -from ipywidgets import DOMWidget from .._version import EXTENSION_NAME, EXTENSION_SPEC_VERSION +from ..styled_widget import StyledWidget from .elk_model import ElkLabel @@ -43,7 +43,7 @@ def message(self) -> Dict: return {"value": self.text.text, "cssClasses": css_classes, "id": self.id} -class ElkTextSizer(DOMWidget): +class ElkTextSizer(StyledWidget): """Jupyterlab widget for getting rendered text sizes from the DOM""" _model_name = T.Unicode("ELKTextSizerModel").tag(sync=True) @@ -56,7 +56,6 @@ class ElkTextSizer(DOMWidget): _request_queue: asyncio.Queue = None _response_queue: asyncio.Queue = None - custom_css = T.List() timeout = T.Float(default_value=0.1, min=0) max_size = T.Int(default_value=100) @@ -71,8 +70,9 @@ def __init__(self, *args, **kwargs): asyncio.create_task(self._process_requests()) asyncio.create_task(self._process_responses()) - @T.observe("custom_css") + @T.observe("style") def _bust_futures_cache(self, change=None): + # TODO handle cache clearing pass def _handle_response(self, _, content, buffers): diff --git a/src/elkschema.ts b/src/elkschema.ts index 4b309416..a5ccdfb6 100644 --- a/src/elkschema.ts +++ b/src/elkschema.ts @@ -7,13 +7,22 @@ */ import * as ELK from 'elkjs'; +export interface ElkProperties { + cssClasses?: string; + LayoutOptions?: object; +} + +export type AnyElkLabelWithProperties = ELK.ElkLabel & { properties?: ElkProperties }; + export interface LazyElkEdge extends ELK.ElkEdge { sources: string[]; targets: string[]; + labels?: AnyElkLabelWithProperties[]; } export interface AnyElkPort extends ELK.ElkPort { - properties?: object; + properties?: ElkProperties; + labels?: AnyElkLabelWithProperties[]; } export type AnyElkEdge = @@ -22,13 +31,12 @@ export type AnyElkEdge = | ELK.ElkPrimitiveEdge | LazyElkEdge; -export type AnyElkEdgeWithProperties = AnyElkEdge & { properties?: object }; -export type AnyElkLabelWithProperties = ELK.ElkLabel & { properties?: object }; +export type AnyElkEdgeWithProperties = AnyElkEdge & { properties?: ElkProperties }; export interface AnyElkNode extends ELK.ElkNode { children?: AnyElkNode[]; ports?: AnyElkPort[]; edges?: AnyElkEdgeWithProperties[]; - properties?: object; + properties?: ElkProperties; labels?: AnyElkLabelWithProperties[]; } diff --git a/src/measure_text.ts b/src/measure_text.ts index 61585542..8bd3892c 100644 --- a/src/measure_text.ts +++ b/src/measure_text.ts @@ -41,7 +41,8 @@ export class ELKTextSizerModel extends DOMWidgetModel { make_container(): HTMLElement { let el: HTMLElement = document.body.appendChild(document.createElement('div')); el.classList.add('p-Widget', ELK_CSS.widget_class, ELK_CSS.sizer_class); - el.innerHTML = `
`; + let raw_css: string = this.get('raw_css').join(''); //TODO should this `raw_css` string be escaped? + el.innerHTML = `
`; return el; } @@ -91,7 +92,9 @@ export class ELKTextSizerModel extends DOMWidgetModel { event: 'measurement', measurements: this.read_sizes(content.texts, elements) }; - // document.body.removeChild(el); + if (!ELK_DEBUG) { + document.body.removeChild(el); + } this.send(response, {}, []); }); } From 058b13376b5e32d9417bd7db7c6cc8918495fdf3 Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Tue, 1 Dec 2020 16:16:28 -0500 Subject: [PATCH 07/20] update changelog. remove erroneous property in the --- CHANGELOG.md | 4 +++- examples/101_text_sizer.ipynb | 5 +++-- examples/simple.json | 5 +---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21b6c88c..6d03a5b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,14 @@ ## @jupyrdf/jupyter-elk 0.2.1 - fix `ElkTransformer` handling of custom properties ([#46][]) +- add `ElkTextSizer` passing through of custom css style when sizing labels ([#48][]) ## ipyelk 0.2.1 -> TBD +- update Elk schema to allow for properties on edge labels and port labels ([#48][]) [#46]: https://github.com/jupyrdf/ipyelk/pull/46 +[#48]: https://github.com/jupyrdf/ipyelk/pull/48 --- diff --git a/examples/101_text_sizer.ipynb b/examples/101_text_sizer.ipynb index aada4b79..025d5def 100644 --- a/examples/101_text_sizer.ipynb +++ b/examples/101_text_sizer.ipynb @@ -91,9 +91,10 @@ "source": [ "num = 10\n", "bulk_sizer = ipyelk.diagram.ElkTextSizer(\n", - " max_size=num, style={\n", + " max_size=num,\n", + " style={\n", " \" .sprotty .elklabel.larger-font\": {\"font-size\": \"20px\"},\n", - " }\n", + " },\n", ")" ] }, diff --git a/examples/simple.json b/examples/simple.json index f76376b2..9ae21508 100644 --- a/examples/simple.json +++ b/examples/simple.json @@ -87,8 +87,5 @@ } } ], - "id": "root", - "properties": { - "algorithm": "layered" - } + "id": "root" } From 89b10e9d9a0ed71ded153a98eac0bd130e4a774d Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Wed, 2 Dec 2020 08:48:50 -0500 Subject: [PATCH 08/20] removed layoutOption from in schema. add merging of layout options if specified on the node --- CHANGELOG.md | 5 ++++- py_src/ipyelk/nx/transformer.py | 7 +++++-- src/elkschema.ts | 1 - 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d03a5b3..ae133c67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,10 @@ ## ipyelk 0.2.1 -- update Elk schema to allow for properties on edge labels and port labels ([#48][]) +- update Elk schema to allow for properties (and c) on edge labels and port labels + ([#48][]) +- Merge layout options if specified in a given node's data with default layout options + ([#48][]) [#46]: https://github.com/jupyrdf/ipyelk/pull/46 [#48]: https://github.com/jupyrdf/ipyelk/pull/48 diff --git a/py_src/ipyelk/nx/transformer.py b/py_src/ipyelk/nx/transformer.py index 8aea3588..28c99f9e 100644 --- a/py_src/ipyelk/nx/transformer.py +++ b/py_src/ipyelk/nx/transformer.py @@ -223,8 +223,11 @@ def build_hierarchy(self, elknodes: NodeMap) -> ElkNode: return top # returns top level node async def make_elknode(self, node) -> ElkNode: - layout = self.get_layout(node, ElkNode) - + # merge layout options defined on the node data with default layout options + layout = merge( + self.get_node_data(node).get("layoutOptions", {}), + self.get_layout(node, ElkNode), + ) labels = await self.make_labels(node) # update port map with declared ports in the networkx node data diff --git a/src/elkschema.ts b/src/elkschema.ts index a5ccdfb6..3e124588 100644 --- a/src/elkschema.ts +++ b/src/elkschema.ts @@ -9,7 +9,6 @@ import * as ELK from 'elkjs'; export interface ElkProperties { cssClasses?: string; - LayoutOptions?: object; } export type AnyElkLabelWithProperties = ELK.ElkLabel & { properties?: ElkProperties }; From 3bc27217f5ad7eb5d228080a11799bfeb9dd3882 Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Wed, 2 Dec 2020 12:03:56 -0500 Subject: [PATCH 09/20] method moved --- py_src/ipyelk/nx/transformer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py_src/ipyelk/nx/transformer.py b/py_src/ipyelk/nx/transformer.py index 283488f5..602393b8 100644 --- a/py_src/ipyelk/nx/transformer.py +++ b/py_src/ipyelk/nx/transformer.py @@ -144,7 +144,7 @@ async def transform(self) -> ElkNode: elknodes = {node: await self.make_elknode(node) for node in visible_nodes} elknodes[ElkRoot] = top = ElkNode( id=self.ELK_ROOT_ID, - children=self.build_hierarchy(elknodes), + children=build_hierarchy(g, tree, elknodes, self.HIDDEN_ATTR), layoutOptions=self.get_layout(ElkRoot, "parents"), ) From 242907ad6acdc28ecb844a1e50a61d12a0d682c9 Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Wed, 2 Dec 2020 13:19:44 -0500 Subject: [PATCH 10/20] moving text sizing functions up to --- py_src/ipyelk/nx/nx.py | 82 +++++++++++------ py_src/ipyelk/nx/transformer.py | 151 +++----------------------------- py_src/ipyelk/transform.py | 122 ++++++++++++++++++++++++-- 3 files changed, 180 insertions(+), 175 deletions(-) diff --git a/py_src/ipyelk/nx/nx.py b/py_src/ipyelk/nx/nx.py index 2d239a8d..1463d1da 100644 --- a/py_src/ipyelk/nx/nx.py +++ b/py_src/ipyelk/nx/nx.py @@ -1,39 +1,12 @@ # Copyright (c) 2020 Dane Freeman. # Distributed under the terms of the Modified BSD License. -from dataclasses import dataclass from itertools import tee from typing import Dict, Hashable, List, Optional, Tuple import networkx as nx -from ..diagram.elk_model import ElkNode, ElkPort, ElkRoot - - -@dataclass(frozen=True) -class Edge: - source: Hashable - source_port: Optional[Hashable] - target: Hashable - target_port: Optional[Hashable] - owner: Hashable - data: Optional[Dict] - - def __hash__(self): - return hash((self.source, self.source_port, self.target, self.target_port)) - - -@dataclass(frozen=True) -class Port: - node: Hashable - elkport: ElkPort - - def __hash__(self): - return hash(tuple([hash(self.node), hash(self.elkport.id)])) - - -NodeMap = Dict[Hashable, ElkNode] -EdgeMap = Dict[Hashable, List[Edge]] -PortMap = Dict[Hashable, Port] +from ..diagram.elk_model import ElkNode, ElkRoot +from ..transform import NodeMap def compact(array: Optional[List]) -> Optional[List]: @@ -107,3 +80,54 @@ def get_ports(edge_data: Dict) -> Tuple[Optional[Hashable], Optional[Hashable]]: source_port = edge_data.get("sourcePort", None) or p target_port = edge_data.get("targetPort", None) or p return source_port, target_port + + +def is_hidden(tree: nx.DiGraph, node: Hashable, attr: str) -> bool: + """Iterate on the node ancestors and determine if it is hidden along the chain""" + if tree and node in tree: + if tree.nodes[node].get(attr, False): + return True + for ancestor in nx.ancestors(tree, node): + if tree.nodes[ancestor].get(attr, False): + return True + return False + + +def build_hierarchy( + g: nx.Graph, tree: nx.DiGraph, elknodes: NodeMap, HIDDEN_ATTR: str +) -> List[ElkNode]: + """The Elk JSON is hierarchical. This method iterates through the build + elknodes and links children to parents if the incoming source includes a + hierarcichal networkx diagraph tree. + + :param g: [description] + :type g: nx.Graph + :param tree: [description] + :type tree: nx.DiGraph + :param elknodes: mapping of networkx nodes to their elknode representations + :type elknodes: NodeMap + :param HIDDEN_ATTR: [description] + :type HIDDEN_ATTR: str + :return: Top level ElkNodes to put as children under the ElkRoot + :rtype: List[ElkNode] + """ + if tree: + # roots of the tree + roots = [n for n, d in tree.in_degree() if d == 0] + for n, elknode in elknodes.items(): + if n in tree: + elknode.children = [ + elknodes[c] + for c in tree.neighbors(n) + if not is_hidden(tree, c, HIDDEN_ATTR) + ] + else: + # nodes that are not in the tree + roots.append(n) + else: + # only flat graph provided + roots = [] + for n, elknode in elknodes.items(): + if not is_hidden(tree, n, HIDDEN_ATTR): + roots.append(n) + return [elknodes[n] for n in roots] diff --git a/py_src/ipyelk/nx/transformer.py b/py_src/ipyelk/nx/transformer.py index 602393b8..db958841 100644 --- a/py_src/ipyelk/nx/transformer.py +++ b/py_src/ipyelk/nx/transformer.py @@ -19,17 +19,17 @@ ElkRoot, ) from ..diagram.elk_text_sizer import ElkTextSizer, size_labels -from ..transform import ElkTransformer -from .nx import ( +from ..transform import ( Edge, EdgeMap, + ElkTransformer, NodeMap, Port, PortMap, - compact, - get_ports, - lowest_common_ancestor, + collect_labels, + merge, ) +from .nx import build_hierarchy, compact, get_ports, is_hidden, lowest_common_ancestor class XELK(ElkTransformer): @@ -49,12 +49,14 @@ class XELK(ElkTransformer): label_key = T.Unicode(default_value="labels") port_key = T.Unicode(default_value="ports") - text_sizer: ElkTextSizer = T.Instance(ElkTextSizer, kw={}, allow_none=True) - @T.default("source") def _default_source(self): return (nx.Graph(), None) + @T.default("text_sizer") + def _default_text_sizer(self): + return ElkTextSizer() + @T.default("layouts") def _default_layouts(self): parent_opts = opt.OptionsWidget( @@ -175,7 +177,7 @@ async def transform(self) -> ElkNode: # self.register(label, port) # bulk calculate label sizes - await size_labels(self.text_sizer, self.collect_labels(elknodes)) + await size_labels(self.text_sizer, collect_labels([top])) # link children to parents self._nodes = elknodes @@ -444,29 +446,6 @@ async def collect_ports(self, *nodes) -> PortMap: ports[elkport.id] = Port(node=node, elkport=elkport) return ports - def collect_labels(self, nodes: NodeMap) -> Tuple[ElkLabel]: - """Iterate over the map of ElkNodes and pluck labels from: - * node - * node.ports - * node.edges - - :param nodes: [description] - :type nodes: NodeMap - :return: [description] - :rtype: Tuple[ElkLabel] - """ - labels = [] - for elknode in nodes.values(): - for label in listed(elknode.labels): - labels.append(label) - for elkport in listed(elknode.ports): - for label in listed(elkport.labels): - labels.append(label) - for elkedge in listed(elknode.edges): - for label in listed(elkedge.labels): - labels.append(label) - return tuple(labels) - async def make_labels(self, node: Hashable) -> Optional[List[ElkLabel]]: labels = [] g = self.source[0] @@ -602,113 +581,3 @@ def closest_common_visible(self, nodes: Tuple[Hashable]) -> Hashable: return ElkRoot result = lowest_common_ancestor(tree, [self.closest_visible(n) for n in nodes]) return result - - def from_id(self, element_id: str) -> Hashable: - """Use the elk identifiers to find original objects""" - try: - return self._elk_to_item[element_id] - except KeyError: - raise ValueError(f"Element id `{element_id}` not in elk id registry.") - - def to_id(self, item: Hashable) -> str: - """Use original objects to find elk id""" - try: - return self._item_to_elk[item] - except KeyError: - raise ValueError(f"Item `{item}` not in elk id registry.") - - -def is_hidden(tree: nx.DiGraph, node: Hashable, attr: str) -> bool: - """Iterate on the node ancestors and determine if it is hidden along the chain""" - if tree and node in tree: - if tree.nodes[node].get(attr, False): - return True - for ancestor in nx.ancestors(tree, node): - if tree.nodes[ancestor].get(attr, False): - return True - return False - - -def merge(d1: Optional[Dict], d2: Optional[Dict]) -> Optional[Dict]: - """Merge two dictionaries while first testing if either are `None`. - The first dictionary's keys take precedence over the second dictionary. - If the final merged dictionary is empty `None` is returned. - - :param d1: primary dictionary - :type d1: Optional[Dict] - :param d2: secondary dictionary - :type d2: Optional[Dict] - :return: merged dictionary - :rtype: Dict - """ - if d1 is None: - d1 = {} - if d2 is None: - d2 = {} - - cl1 = d1.get("cssClasses", "") - cl2 = d2.get("cssClasses", "") - cl = " ".join(sorted(set([*cl1.split(), *cl2.split()]))).strip() - - value = {**d2, **d1} # right most wins if duplicated keys - - # if either had cssClasses, update that - if cl: - value["cssClasses"] = cl - - if value: - return value - - -def listed(values: Optional[List]) -> List: - """Checks if incoming `values` is None then either returns a new list or - original value. - - :param values: List of values - :type values: Optional[List] - :return: List of values or empty list - :rtype: List - """ - if values is None: - return [] - return values - - -def build_hierarchy( - g: nx.Graph, tree: nx.DiGraph, elknodes: NodeMap, HIDDEN_ATTR: str -) -> List[ElkNode]: - """The Elk JSON is hierarchical. This method iterates through the build - elknodes and links children to parents if the incoming source includes a - hierarcichal networkx diagraph tree. - - :param g: [description] - :type g: nx.Graph - :param tree: [description] - :type tree: nx.DiGraph - :param elknodes: mapping of networkx nodes to their elknode representations - :type elknodes: NodeMap - :param HIDDEN_ATTR: [description] - :type HIDDEN_ATTR: str - :return: Top level ElkNodes to put as children under the ElkRoot - :rtype: List[ElkNode] - """ - if tree: - # roots of the tree - roots = [n for n, d in tree.in_degree() if d == 0] - for n, elknode in elknodes.items(): - if n in tree: - elknode.children = [ - elknodes[c] - for c in tree.neighbors(n) - if not is_hidden(tree, c, HIDDEN_ATTR) - ] - else: - # nodes that are not in the tree - roots.append(n) - else: - # only flat graph provided - roots = [] - for n, elknode in elknodes.items(): - if not is_hidden(tree, n, HIDDEN_ATTR): - roots.append(n) - return [elknodes[n] for n in roots] diff --git a/py_src/ipyelk/transform.py b/py_src/ipyelk/transform.py index e386acb3..3d8cdadf 100644 --- a/py_src/ipyelk/transform.py +++ b/py_src/ipyelk/transform.py @@ -1,17 +1,46 @@ # Copyright (c) 2020 Dane Freeman. # Distributed under the terms of the Modified BSD License. import asyncio -from typing import Dict, Hashable, Optional, Union +from dataclasses import dataclass +from typing import Dict, Hashable, List, Optional, Union import ipywidgets as W import traitlets as T -from .diagram import ElkDiagram, ElkLabel, ElkNode +from .diagram import ElkDiagram, ElkLabel, ElkNode, ElkPort from .diagram.elk_model import ElkGraphElement +from .diagram.elk_text_sizer import ElkTextSizer, size_labels from .schema import ElkSchemaValidator from .trait_types import Schema +@dataclass(frozen=True) +class Edge: + source: Hashable + source_port: Optional[Hashable] + target: Hashable + target_port: Optional[Hashable] + owner: Hashable + data: Optional[Dict] + + def __hash__(self): + return hash((self.source, self.source_port, self.target, self.target_port)) + + +@dataclass(frozen=True) +class Port: + node: Hashable + elkport: ElkPort + + def __hash__(self): + return hash(tuple([hash(self.node), hash(self.elkport.id)])) + + +NodeMap = Dict[Hashable, ElkNode] +EdgeMap = Dict[Hashable, List[Edge]] +PortMap = Dict[Hashable, Port] + + class ElkTransformer(W.Widget): """ Transform data into the form required by the ElkDiagram. """ @@ -25,6 +54,8 @@ class ElkTransformer(W.Widget): _elk_to_item: Dict[str, Hashable] = None _item_to_elk: Dict[Hashable, str] = None + text_sizer: ElkTextSizer = T.Instance(ElkTextSizer, allow_none=True) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -32,7 +63,10 @@ def __init__(self, *args, **kwargs): async def transform(self) -> ElkNode: """Generate elk json""" - return ElkNode(**self.source) + top = ElkNode(**self.source) + # bulk calculate label sizes + await size_labels(self.text_sizer, collect_labels([top])) + return top @T.default("value") def _default_value(self): @@ -62,11 +96,17 @@ def refresh(self, change: T.Bunch = None): def from_id(self, element_id: str) -> Hashable: """Use the elk identifiers to find original objects""" - return element_id + try: + return self._elk_to_item[element_id] + except KeyError: + raise ValueError(f"Element id `{element_id}` not in elk id registry.") def to_id(self, item: Hashable) -> str: """Use original objects to find elk id""" - return item + try: + return self._item_to_elk[item] + except KeyError: + raise ValueError(f"Item `{item}` not in elk id registry.") def connect(self, view: ElkDiagram) -> T.link: """Connect the output value of this transformer to a diagram""" @@ -95,3 +135,75 @@ class ElkDuplicateIDError(Exception): """Elk Ids must be unique""" pass + + +def merge(d1: Optional[Dict], d2: Optional[Dict]) -> Optional[Dict]: + """Merge two dictionaries while first testing if either are `None`. + The first dictionary's keys take precedence over the second dictionary. + If the final merged dictionary is empty `None` is returned. + + :param d1: primary dictionary + :type d1: Optional[Dict] + :param d2: secondary dictionary + :type d2: Optional[Dict] + :return: merged dictionary + :rtype: Dict + """ + if d1 is None: + d1 = {} + if d2 is None: + d2 = {} + + cl1 = d1.get("cssClasses", "") + cl2 = d2.get("cssClasses", "") + cl = " ".join(sorted(set([*cl1.split(), *cl2.split()]))).strip() + + value = {**d2, **d1} # right most wins if duplicated keys + + # if either had cssClasses, update that + if cl: + value["cssClasses"] = cl + + if value: + return value + + +def listed(values: Optional[List]) -> List: + """Checks if incoming `values` is None then either returns a new list or + original value. + + :param values: List of values + :type values: Optional[List] + :return: List of values or empty list + :rtype: List + """ + if values is None: + return [] + return values + + +def collect_labels(nodes: List[ElkNode], recurse: bool = True) -> List[ElkLabel]: + """Iterate over the map of ElkNodes and pluck labels from: + * node + * node.ports + * node.edges + If recuse is True then labels from each child will be included + + :param nodes: [description] + :type nodes: List[ElkNode] + :return: [description] + :rtype: List[ElkLabel] + """ + labels = [] + for elknode in nodes: + for label in listed(elknode.labels): + labels.append(label) + for elkport in listed(elknode.ports): + for label in listed(elkport.labels): + labels.append(label) + for elkedge in listed(elknode.edges): + for label in listed(elkedge.labels): + labels.append(label) + if recurse: + labels.extend(collect_labels(listed(elknode.children))) + return labels From 108f6e193bc29a907058e9c99eb0696ea37aa36c Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Wed, 2 Dec 2020 14:44:41 -0500 Subject: [PATCH 11/20] Add Unit Test Scaffold (#52) * start unit test skeleton, ci * upload all reports * add hypothesis-jsonschema * don't use xdist by default * rework xdist, etc * more work on test reporting, etc * fix more test report stuff * emojiiiii * lint reporter * try some more whitespace in gha * add ansi2html --- .github/workflows/ci.yml | 9 +- .gitignore | 7 +- anaconda-project-lock.yml | 212 ++++++++++++++++++------------- anaconda-project.yml | 25 +++- dodo.py | 73 ++++++++--- py_src/ipyelk/tests/__init__.py | 2 + py_src/ipyelk/tests/conftest.py | 2 + py_src/ipyelk/tests/test_meta.py | 8 ++ scripts/project.py | 9 ++ scripts/reporter.py | 53 ++++++++ scripts/utils.py | 30 +++++ setup.cfg | 38 ++++++ 12 files changed, 355 insertions(+), 113 deletions(-) create mode 100644 py_src/ipyelk/tests/__init__.py create mode 100644 py_src/ipyelk/tests/conftest.py create mode 100644 py_src/ipyelk/tests/test_meta.py create mode 100644 scripts/reporter.py create mode 100644 scripts/utils.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3efaee9..3b4314df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,9 +93,12 @@ jobs: name: ipyelk dist ${{ github.run_number }} path: ./dist - - name: report (robot) + - name: reports uses: actions/upload-artifact@v2 with: - name: ${{ job.status }} Robot ${{ matrix.os }} ${{ github.run_number }} - path: ./atest/output + name: ${{ job.status }} reports ${{ matrix.os }} ${{ github.run_number }} + path: | + ./atest/output + ./build/htmcov + ./build/pytest.html if: always() diff --git a/.gitignore b/.gitignore index a1d34c49..ca94dbaf 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ nosetests.xml coverage.xml *,cover .hypothesis/ +.pytest_cache/ # Translations *.mo @@ -67,8 +68,10 @@ docs/source/_static/embed-bundle.js.map # PyBuilder target/ -# IPython Notebook +# Jupyter .ipynb_checkpoints +untitled* +Untitled* # pyenv .python-version @@ -165,5 +168,3 @@ py_src/ipyelk/schema/*.json .doit* .pabotsuitenames atest/output -untitled* -Untitled* diff --git a/anaconda-project-lock.yml b/anaconda-project-lock.yml index 48e75ce5..2345bd1e 100644 --- a/anaconda-project-lock.yml +++ b/anaconda-project-lock.yml @@ -17,15 +17,20 @@ locking_enabled: true env_specs: ipyelk: locked: false + _lint: + locked: false + _utest: + locked: false default: locked: true - env_spec_hash: 9cb03abb34e23307dfc39011e13ac5934092547b + env_spec_hash: b00fef94d2a2f103b777c177b56f90dfcf7fdc44 platforms: - linux-64 - osx-64 - win-64 packages: all: + - apipkg=1.5=py_0 - appdirs=1.4.4=pyh9f0ad1d_0 - async-lru=1.0.2=py_0 - async_generator=1.10=py_0 @@ -41,11 +46,15 @@ env_specs: - decorator=4.4.2=py_0 - defusedxml=0.6.0=py_0 - entrypoints=0.3=pyhd8ed1ab_1003 + - execnet=1.7.1=py_0 - flake8=3.8.4=py_0 + - hypothesis-jsonschema=0.18.2=pyhd8ed1ab_0 + - hypothesis=5.41.4=pyhd8ed1ab_0 - idna=2.10=pyh9f0ad1d_0 - - importlib-metadata=2.0.0=py_1 - - importlib_metadata=2.0.0=1 + - importlib-metadata=3.1.1=pyhd8ed1ab_0 + - importlib_metadata=3.1.1=hd8ed1ab_0 - importnb=0.7.0=pyhd8ed1ab_0 + - iniconfig=1.1.1=pyh9f0ad1d_0 - ipython_genutils=0.2.0=py_1 - ipywidgets=7.5.1=pyh9f0ad1d_1 - isort=4.3.21=py37hc8dfbb8_1 @@ -56,11 +65,10 @@ env_specs: - jupyterlab=1.2.16=py_0 - jupyterlab_pygments=0.1.2=pyh9f0ad1d_0 - jupyterlab_server=1.2.0=py_0 - - keyring=21.4.0=py37hc8dfbb8_2 - mccabe=0.6.1=py_1 + - more-itertools=8.6.0=pyhd8ed1ab_0 - mypy_extensions=0.4.3=py37hc8dfbb8_2 - nbclient=0.5.1=py_0 - - nbconvert=6.0.7=py37hc8dfbb8_2 - nbformat=5.0.8=py_0 - nest-asyncio=1.4.3=pyhd8ed1ab_0 - networkx=2.5=py_0 @@ -69,16 +77,22 @@ env_specs: - parso=0.7.1=pyh9f0ad1d_0 - pathspec=0.8.1=pyhd3deb0d_0 - pickleshare=0.7.5=py_1003 - - pip=20.2.4=py_0 + - pip=20.3=pyhd8ed1ab_0 - pkginfo=1.6.1=pyh9f0ad1d_0 - - prometheus_client=0.8.0=pyh9f0ad1d_0 + - prometheus_client=0.9.0=pyhd3deb0d_0 - prompt-toolkit=3.0.8=pyha770c72_0 + - py=1.9.0=pyh9f0ad1d_0 - pycodestyle=2.6.0=pyh9f0ad1d_0 - pycparser=2.20=pyh9f0ad1d_2 - pyflakes=2.2.0=pyh9f0ad1d_0 - pygments=2.7.2=py_0 - - pyopenssl=19.1.0=py_1 + - pyopenssl=20.0.0=pyhd8ed1ab_0 - pyparsing=2.4.7=pyh9f0ad1d_0 + - pytest-cov=2.10.1=pyh9f0ad1d_0 + - pytest-forked=1.2.0=pyh9f0ad1d_0 + - pytest-html=3.0.0=pyhd8ed1ab_0 + - pytest-metadata=1.11.0=pyhd3deb0d_0 + - pytest-xdist=2.1.0=py_0 - python-dateutil=2.8.1=py_0 - python_abi=3.7=1_cp37m - pytz=2020.4=pyhd8ed1ab_0 @@ -88,53 +102,56 @@ env_specs: - rfc3986=1.4.0=pyh9f0ad1d_0 - send2trash=1.5.0=py_0 - six=1.15.0=pyh9f0ad1d_0 + - sortedcontainers=2.3.0=pyhd8ed1ab_0 - testpath=0.4.4=py_0 - toml=0.10.2=pyhd8ed1ab_0 - - tqdm=4.51.0=pyh9f0ad1d_0 + - tqdm=4.54.0=pyhd8ed1ab_0 - traitlets=5.0.5=py_0 - twine=3.2.0=py37hc8dfbb8_1 - typing_extensions=3.7.4.3=py_0 - urllib3=1.25.11=py_0 - wcwidth=0.2.5=pyh9f0ad1d_2 - webencodings=0.5.1=py_1 - - wheel=0.35.1=pyh9f0ad1d_0 - - widgetsnbextension=3.5.1=py37hc8dfbb8_4 + - wheel=0.36.0=pyhd3deb0d_0 - zipp=3.4.0=py_0 unix: - - ipykernel=5.3.4=py37hc6149b9_1 - - libblas=3.9.0=2_openblas - - libcblas=3.9.0=2_openblas - - liblapack=3.9.0=2_openblas + - libblas=3.9.0=3_openblas + - libcblas=3.9.0=3_openblas + - liblapack=3.9.0=3_openblas - pexpect=4.8.0=pyh9f0ad1d_2 - ptyprocess=0.6.0=py_1001 linux-64: - _libgcc_mutex=0.1=conda_forge - _openmp_mutex=4.5=1_gnu - - argon2-cffi=20.1.0=py37h8f50634_2 + - ansi2html=1.6.0=py37h89c1867_0 + - argon2-cffi=20.1.0=py37h4abf009_2 - brotlipy=0.7.0=py37hb5d75c8_1001 - ca-certificates=2020.11.8=ha878542_0 - certifi=2020.11.8=py37h89c1867_0 - - cffi=1.14.3=py37h00ebd2e_1 + - cffi=1.14.4=py37hc58025e_1 - chardet=3.0.4=py37he5f6b98_1008 - cmarkgfm=0.4.2=py37h8f50634_3 + - coverage=5.3=py37h8f50634_1 - cryptography=3.2.1=py37hc72a4ac_0 - dbus=1.13.6=hfdff14a_1 - docutils=0.16=py37he5f6b98_2 - expat=2.2.9=he1b5a44_2 - - gettext=0.19.8.1=hf34092f_1004 - - glib=2.66.2=h58526e2_0 - - icu=67.1=he1b5a44_0 + - gettext=0.19.8.1=h0b5b191_1005 + - glib=2.66.3=h9c3ff4c_1 + - icu=68.1=h58526e2_0 - importlib_resources=3.3.0=py37h89c1867_0 + - ipykernel=5.3.4=py37h888b3d9_1 - ipython=7.19.0=py37h888b3d9_0 - jedi=0.17.2=py37h89c1867_1 - - jeepney=0.4.3=py_0 - - jupyter_core=4.6.3=py37h89c1867_2 - - ld_impl_linux-64=2.35=h769bd43_9 - - libffi=3.2.1=he1b5a44_1007 + - jeepney=0.6.0=pyhd8ed1ab_0 + - jupyter_core=4.7.0=py37h89c1867_0 + - keyring=21.5.0=py37h89c1867_0 + - ld_impl_linux-64=2.35.1=hed1e6ac_0 + - libffi=3.3=h58526e2_1 - libgcc-ng=9.3.0=h5dbcf3e_17 - libgfortran-ng=9.3.0=he4bcb1c_17 - libgfortran5=9.3.0=he4bcb1c_17 - - libglib=2.66.2=hbe7bbb4_0 + - libglib=2.66.3=h1f3bc88_1 - libgomp=9.3.0=h5dbcf3e_17 - libiconv=1.16=h516909a_0 - libopenblas=0.3.12=pthreads_h4812303_1 @@ -143,48 +160,57 @@ env_specs: - libuv=1.40.0=hd18ef5c_0 - markupsafe=1.1.1=py37hb5d75c8_2 - mistune=0.8.4=py37h4abf009_1002 - - ncurses=6.2=h58526e2_3 - - nodejs=14.14.0=h914e61d_0 + - nbconvert=6.0.7=py37h89c1867_3 + - ncurses=6.2=h58526e2_4 + - nodejs=14.15.1=h25f6087_0 - notebook=6.1.5=py37h89c1867_0 - numpy=1.19.4=py37h7e9df27_1 - openssl=1.1.1h=h516909a_0 - pandas=1.1.4=py37h10a2094_0 - - pandoc=2.11.1.1=h36c2ea0_0 + - pandoc=2.11.2=h36c2ea0_0 - pcre=8.44=he1b5a44_0 + - pluggy=0.13.1=py37he5f6b98_3 - pyrsistent=0.17.3=py37h4abf009_1 - pysocks=1.7.1=py37he5f6b98_2 - - python=3.7.8=h6f2ec95_1_cpython - - pyzmq=19.0.2=py37hac76be4_2 + - pytest-asyncio=0.14.0=py37h89c1867_0 + - pytest=6.1.2=py37h89c1867_0 + - python=3.7.8=hffdb5ce_3_cpython + - pyzmq=20.0.0=py37h5a562af_1 - readline=8.0=he28a2e2_2 - - regex=2020.11.11=py37h4abf009_0 - - secretstorage=3.2.0=py37h89c1867_0 + - regex=2020.11.13=py37h4abf009_0 + - secretstorage=3.3.0=py37h89c1867_0 - setuptools=49.6.0=py37he5f6b98_2 - - sqlite=3.33.0=h4cf870e_1 + - sqlite=3.34.0=h74cdb3f_0 - terminado=0.9.1=py37h89c1867_1 - tk=8.6.10=hed695b0_1 - tornado=6.1=py37h4abf009_0 - typed-ast=1.4.1=py37h4abf009_1 + - widgetsnbextension=3.5.1=py37h89c1867_4 - xz=5.2.5=h516909a_1 - - zeromq=4.3.3=h58526e2_2 + - zeromq=4.3.3=h58526e2_3 - zlib=1.2.11=h516909a_1010 osx-64: - - appnope=0.1.0=py37hf985489_1002 - - argon2-cffi=20.1.0=py37h60d8a13_2 + - ansi2html=1.6.0=py37hf985489_0 + - appnope=0.1.2=py37hf985489_0 + - argon2-cffi=20.1.0=py37h4b544eb_2 - brotlipy=0.7.0=py37h395d20d_1001 - ca-certificates=2020.11.8=h033912b_0 - certifi=2020.11.8=py37hf985489_0 - - cffi=1.14.3=py37h446cb54_1 + - cffi=1.14.4=py37hc5b2277_1 - chardet=3.0.4=py37h2987424_1008 - cmarkgfm=0.4.2=py37h60d8a13_3 + - coverage=5.3=py37h60d8a13_1 - cryptography=3.2.1=py37h3b7a55b_0 - docutils=0.16=py37h2987424_2 - - icu=67.1=hb1e8313_0 + - icu=68.1=h74dc148_0 - importlib_resources=3.3.0=py37hf985489_0 + - ipykernel=5.3.4=py37he01cfaa_1 - ipython=7.19.0=py37he01cfaa_0 - jedi=0.17.2=py37hf985489_1 - - jupyter_core=4.6.3=py37hf985489_2 - - libcxx=11.0.0=h439d374_0 - - libffi=3.2.1=hb1e8313_1007 + - jupyter_core=4.7.0=py37hf985489_0 + - keyring=21.5.0=py37hf985489_0 + - libcxx=11.0.0=h4c3b8ed_1 + - libffi=3.3=h74dc148_1 - libgfortran5=9.3.0=h7cc5361_13 - libgfortran=5.0.0=h7cc5361_13 - libopenblas=0.3.12=openmp_h54245bb_1 @@ -193,36 +219,44 @@ env_specs: - llvm-openmp=11.0.0=h73239a0_1 - markupsafe=1.1.1=py37h395d20d_2 - mistune=0.8.4=py37h4b544eb_1002 - - ncurses=6.2=h2e338ed_3 - - nodejs=14.14.0=h29ef208_0 + - nbconvert=6.0.7=py37hf985489_3 + - ncurses=6.2=h2e338ed_4 + - nodejs=14.15.1=heaeed3d_0 - notebook=6.1.5=py37hf985489_0 - numpy=1.19.4=py37h9ebeaaa_1 - openssl=1.1.1h=haf1e3a3_0 - pandas=1.1.4=py37h9b0e0a3_0 - - pandoc=2.11.1.1=hbcb3906_0 + - pandoc=2.11.2=hc929b4f_0 + - pluggy=0.13.1=py37h2987424_3 - pyrsistent=0.17.3=py37h4b544eb_1 - pysocks=1.7.1=py37h2987424_2 - - python=3.7.8=hc9dea61_1_cpython - - pyzmq=19.0.2=py37hf1e22d8_2 + - pytest-asyncio=0.14.0=py37hf985489_0 + - pytest=6.1.2=py37hf985489_0 + - python=3.7.8=h4f09611_3_cpython + - pyzmq=20.0.0=py37h47fd9b3_1 - readline=8.0=h0678c8f_2 - - regex=2020.11.11=py37h4b544eb_0 + - regex=2020.11.13=py37h4b544eb_0 - setuptools=49.6.0=py37h2987424_2 - - sqlite=3.33.0=h960bd1c_1 + - sqlite=3.34.0=h17101e1_0 - terminado=0.9.1=py37hf985489_1 - tk=8.6.10=hb0a8c7a_1 - tornado=6.1=py37h4b544eb_0 - typed-ast=1.4.1=py37h4b544eb_1 + - widgetsnbextension=3.5.1=py37hf985489_4 - xz=5.2.5=haf1e3a3_1 - - zeromq=4.3.3=h2e338ed_2 + - zeromq=4.3.3=h74dc148_3 - zlib=1.2.11=h7795811_1010 win-64: - - argon2-cffi=20.1.0=py37h4ab8f01_2 + - ansi2html=1.6.0=py37h03978a9_0 + - argon2-cffi=20.1.0=py37hcc03f2d_2 + - atomicwrites=1.4.0=pyh9f0ad1d_0 - brotlipy=0.7.0=py37h0013d47_1001 - ca-certificates=2020.11.8=h5b45459_0 - certifi=2020.11.8=py37h03978a9_0 - - cffi=1.14.3=py37hd6b71e5_1 + - cffi=1.14.4=py37hd8e9650_1 - chardet=3.0.4=py37hf50a25e_1008 - cmarkgfm=0.4.2=py37h4ab8f01_3 + - coverage=5.3=py37h4ab8f01_1 - cryptography=3.2.1=py37hd8e9650_0 - docutils=0.16=py37hf50a25e_2 - importlib_resources=3.3.0=py37h03978a9_0 @@ -230,10 +264,11 @@ env_specs: - ipykernel=5.3.4=py37h7b7c402_1 - ipython=7.19.0=py37heaed05f_0 - jedi=0.17.2=py37h03978a9_1 - - jupyter_core=4.6.3=py37h03978a9_2 - - libblas=3.8.0=20_mkl - - libcblas=3.8.0=20_mkl - - liblapack=3.8.0=20_mkl + - jupyter_core=4.7.0=py37h03978a9_0 + - keyring=21.5.0=py37h03978a9_0 + - libblas=3.8.0=21_mkl + - libcblas=3.8.0=21_mkl + - liblapack=3.8.0=21_mkl - libsodium=1.0.18=h8d14728_1 - m2w64-gcc-libgfortran=5.3.0=6 - m2w64-gcc-libs-core=5.3.0=7 @@ -242,33 +277,38 @@ env_specs: - m2w64-libwinpthread-git=5.0.0.4634.697f757=2 - markupsafe=1.1.1=py37h0013d47_2 - mistune=0.8.4=py37hcc03f2d_1002 - - mkl=2020.2=256 + - mkl=2020.4=hb70f87d_311 - msys2-conda-epoch=20160418=1 - - nodejs=14.14.0=h57928b3_0 + - nbconvert=6.0.7=py37h03978a9_3 + - nodejs=14.15.1=h57928b3_0 - notebook=6.1.5=py37h03978a9_0 - numpy=1.19.4=py37hd20adf4_1 - openssl=1.1.1h=he774522_0 - pandas=1.1.4=py37h08fd248_0 - - pandoc=2.11.1.1=h8ffe710_0 + - pandoc=2.11.2=h8ffe710_0 + - pluggy=0.13.1=py37hf50a25e_3 - pyrsistent=0.17.3=py37hcc03f2d_1 - pysocks=1.7.1=py37hf50a25e_2 - - python=3.7.8=h60c2a47_1_cpython + - pytest-asyncio=0.14.0=py37h03978a9_0 + - pytest=6.1.2=py37h03978a9_0 + - python=3.7.8=h7840368_3_cpython - pywin32-ctypes=0.2.0=py37hc8dfbb8_1002 - pywin32=228=py37h4ab8f01_0 - pywinpty=0.5.7=py37hc8dfbb8_1 - - pyzmq=19.0.2=py37h453f00a_2 - - regex=2020.11.11=py37hcc03f2d_0 + - pyzmq=20.0.0=py37h0d95fc2_1 + - regex=2020.11.13=py37hcc03f2d_0 - setuptools=49.6.0=py37hf50a25e_2 - - sqlite=3.33.0=he774522_1 + - sqlite=3.34.0=h8ffe710_0 - terminado=0.9.1=py37h03978a9_1 - tornado=6.1=py37hcc03f2d_0 - typed-ast=1.4.1=py37hcc03f2d_1 - vc=14.1=h869be7e_1 - vs2015_runtime=14.16.27012=h30e32a0_2 + - widgetsnbextension=3.5.1=py37h03978a9_4 - win_inet_pton=1.1.0=py37hc8dfbb8_1 - wincertstore=0.2=py37hc8dfbb8_1005 - winpty=0.4.3=4 - - zeromq=4.3.2=ha925a31_4 + - zeromq=4.3.3=h0e60522_3 atest: locked: true env_spec_hash: a3cbc653618884c4eb2c0cda4abc326eac9894ad @@ -279,41 +319,41 @@ env_specs: packages: all: - idna=2.10=pyh9f0ad1d_0 - - pip=20.2.4=py_0 + - pip=20.3=pyhd8ed1ab_0 - pycparser=2.20=pyh9f0ad1d_2 - - pyopenssl=19.1.0=py_1 + - pyopenssl=20.0.0=pyhd8ed1ab_0 - python_abi=3.7=1_cp37m - robotframework-lint=1.1=pyh9f0ad1d_0 - robotframework-pabot=1.10.0=py_0 - - robotframework-pythonlibcore=2.1.0=py_0 + - robotframework-pythonlibcore=2.1.0=pyhd8ed1ab_1 - robotframework-seleniumlibrary=4.5.0=pyh9f0ad1d_0 - robotframework=3.2.2=pyh9f0ad1d_0 - six=1.15.0=pyh9f0ad1d_0 - - urllib3=1.26.1=pyhd8ed1ab_0 - - wheel=0.35.1=pyh9f0ad1d_0 + - urllib3=1.26.2=pyhd8ed1ab_0 + - wheel=0.36.0=pyhd3deb0d_0 linux-64: - _libgcc_mutex=0.1=conda_forge - _openmp_mutex=4.5=1_gnu - brotlipy=0.7.0=py37hb5d75c8_1001 - ca-certificates=2020.11.8=ha878542_0 - certifi=2020.11.8=py37h89c1867_0 - - cffi=1.14.3=py37h00ebd2e_1 + - cffi=1.14.4=py37hc58025e_1 - cryptography=3.2.1=py37hc72a4ac_0 - - firefox=82.0=he1b5a44_0 + - firefox=83.0=h58526e2_0 - geckodriver=0.28.0=h58526e2_0 - - ld_impl_linux-64=2.35=h769bd43_9 - - libffi=3.2.1=he1b5a44_1007 + - ld_impl_linux-64=2.35.1=hed1e6ac_0 + - libffi=3.3=h58526e2_1 - libgcc-ng=9.3.0=h5dbcf3e_17 - libgomp=9.3.0=h5dbcf3e_17 - libstdcxx-ng=9.3.0=h2ae2ef3_17 - - ncurses=6.2=h58526e2_3 + - ncurses=6.2=h58526e2_4 - openssl=1.1.1h=h516909a_0 - pysocks=1.7.1=py37he5f6b98_2 - - python=3.7.8=h6f2ec95_1_cpython + - python=3.7.8=hffdb5ce_3_cpython - readline=8.0=he28a2e2_2 - selenium=3.141.0=py37h8f50634_1002 - setuptools=49.6.0=py37he5f6b98_2 - - sqlite=3.33.0=h4cf870e_1 + - sqlite=3.34.0=h74cdb3f_0 - tk=8.6.10=hed695b0_1 - xz=5.2.5=h516909a_1 - zlib=1.2.11=h516909a_1010 @@ -321,20 +361,20 @@ env_specs: - brotlipy=0.7.0=py37h395d20d_1001 - ca-certificates=2020.11.8=h033912b_0 - certifi=2020.11.8=py37hf985489_0 - - cffi=1.14.3=py37h446cb54_1 + - cffi=1.14.4=py37hc5b2277_1 - cryptography=3.2.1=py37h3b7a55b_0 - - firefox=82.0=hb1e8313_0 + - firefox=83.0=h74dc148_0 - geckodriver=0.28.0=h2e338ed_0 - - libcxx=11.0.0=h439d374_0 - - libffi=3.2.1=hb1e8313_1007 - - ncurses=6.2=h2e338ed_3 + - libcxx=11.0.0=h4c3b8ed_1 + - libffi=3.3=h74dc148_1 + - ncurses=6.2=h2e338ed_4 - openssl=1.1.1h=haf1e3a3_0 - pysocks=1.7.1=py37h2987424_2 - - python=3.7.8=hc9dea61_1_cpython + - python=3.7.8=h4f09611_3_cpython - readline=8.0=h0678c8f_2 - selenium=3.141.0=py37h60d8a13_1002 - setuptools=49.6.0=py37h2987424_2 - - sqlite=3.33.0=h960bd1c_1 + - sqlite=3.34.0=h17101e1_0 - tk=8.6.10=hb0a8c7a_1 - xz=5.2.5=haf1e3a3_1 - zlib=1.2.11=h7795811_1010 @@ -342,16 +382,16 @@ env_specs: - brotlipy=0.7.0=py37h0013d47_1001 - ca-certificates=2020.11.8=h5b45459_0 - certifi=2020.11.8=py37h03978a9_0 - - cffi=1.14.3=py37hd6b71e5_1 + - cffi=1.14.4=py37hd8e9650_1 - cryptography=3.2.1=py37hd8e9650_0 - - firefox=82.0=ha925a31_0 + - firefox=83.0=h0e60522_0 - geckodriver=0.28.0=h39d44d4_0 - openssl=1.1.1h=he774522_0 - pysocks=1.7.1=py37hf50a25e_2 - - python=3.7.8=h60c2a47_1_cpython + - python=3.7.8=h7840368_3_cpython - selenium=3.141.0=py37h4ab8f01_1002 - setuptools=49.6.0=py37hf50a25e_2 - - sqlite=3.33.0=he774522_1 + - sqlite=3.34.0=h8ffe710_0 - vc=14.1=h869be7e_1 - vs2015_runtime=14.16.27012=h30e32a0_2 - win_inet_pton=1.1.0=py37hc8dfbb8_1 diff --git a/anaconda-project.yml b/anaconda-project.yml index 8981c346..355023ae 100644 --- a/anaconda-project.yml +++ b/anaconda-project.yml @@ -37,12 +37,10 @@ env_specs: - win-64 inherit_from: - ipyelk + - _utest + - _lint packages: - - black - - flake8 - - isort <5 - pip - - pyflakes - python >=3.7,<3.8.0a0 - twine - wheel @@ -65,6 +63,7 @@ env_specs: - python >=3.7 atest: + description: acceptance test environment (kept separate because firefox) platforms: - linux-64 - osx-64 @@ -80,3 +79,21 @@ env_specs: - robotframework-lint - robotframework-pabot - robotframework-seleniumlibrary + + _lint: + description: linting/formatting environment. not actually created. + packages: + - black + - isort <5 + - pyflakes + - flake8 + + _utest: + description: unit test environment. not actually created. + packages: + - ansi2html + - hypothesis-jsonschema + - pytest-asyncio + - pytest-cov + - pytest-html + - pytest-xdist diff --git a/dodo.py b/dodo.py index 55554151..37583cf9 100644 --- a/dodo.py +++ b/dodo.py @@ -10,13 +10,16 @@ # Copyright (c) 2020 Dane Freeman. # Distributed under the terms of the Modified BSD License. +import json import os import subprocess from hashlib import sha256 -import scripts.project as P from doit.action import CmdAction from doit.tools import PythonInteractiveAction, config_changed +from scripts import project as P +from scripts import reporter +from scripts import utils as U os.environ["PYTHONIOENCODING"] = "utf-8" @@ -25,9 +28,27 @@ "verbosity": 2, "par_type": "thread", "default_tasks": ["binder"], + "reporter": reporter.GithubActionsReporter, } -COMMIT = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode("utf-8") +COMMIT = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode("utf-8").strip() + + +def task_all(): + """do everything except start lab""" + + return dict( + file_dep=[ + *P.EXAMPLE_HTML, + P.ATEST_CANARY, + P.HTMLCOV_INDEX, + P.OK_PREFLIGHT_LAB, + P.OK_RELEASE, + P.PYTEST_HTML, + P.SHA256SUMS, + ], + actions=([_echo_ok("ALL GOOD")]), + ) def task_preflight(): @@ -273,6 +294,39 @@ def _test(): for nb in P.EXAMPLE_IPYNB: yield _nb_test(nb) + utest_args = [ + *P.APR_DEFAULT, + *P.PYM, + "pytest", + "--cov-fail-under", + str(P.PYTEST_COV_THRESHOLD), + ] + + if P.UTEST_PROCESSES: + utest_args += ["-n", P.UTEST_PROCESSES] + + pytest_args = os.environ.get("PYTEST_ARGS", "").strip() + + if pytest_args: + try: + utest_args += json.loads(pytest_args) + except Exception as err: + print(err) + + yield dict( + name="utest", + doc="run unit tests with pytest", + uptodate=[config_changed(COMMIT)], + file_dep=[*P.ALL_PY_SRC, P.SETUP_CFG, P.OK_PIP_INSTALL], + targets=[P.HTMLCOV_INDEX, P.PYTEST_HTML, P.PYTEST_XUNIT], + actions=[ + utest_args, + lambda: U.strip_timestamps( + *P.HTMLCOV.rglob("*.html"), P.PYTEST_HTML, slug=COMMIT + ), + ], + ) + def _pabot_logs(): for robot_out in sorted(P.ATEST_OUT.rglob("robot_*.out")): print(f"\n[{robot_out.relative_to(P.ROOT)}]") @@ -490,21 +544,6 @@ def _watch(): ) -def task_all(): - """do everything except start lab""" - - return dict( - file_dep=[ - *P.EXAMPLE_HTML, - P.ATEST_CANARY, - P.OK_PREFLIGHT_LAB, - P.OK_RELEASE, - P.SHA256SUMS, - ], - actions=([_echo_ok("ALL GOOD")]), - ) - - def _echo_ok(msg): def _echo(): print(msg, flush=True) diff --git a/py_src/ipyelk/tests/__init__.py b/py_src/ipyelk/tests/__init__.py new file mode 100644 index 00000000..be9ea83c --- /dev/null +++ b/py_src/ipyelk/tests/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2020 Dane Freeman. +# Distributed under the terms of the Modified BSD License. diff --git a/py_src/ipyelk/tests/conftest.py b/py_src/ipyelk/tests/conftest.py new file mode 100644 index 00000000..be9ea83c --- /dev/null +++ b/py_src/ipyelk/tests/conftest.py @@ -0,0 +1,2 @@ +# Copyright (c) 2020 Dane Freeman. +# Distributed under the terms of the Modified BSD License. diff --git a/py_src/ipyelk/tests/test_meta.py b/py_src/ipyelk/tests/test_meta.py new file mode 100644 index 00000000..ea17fd6b --- /dev/null +++ b/py_src/ipyelk/tests/test_meta.py @@ -0,0 +1,8 @@ +# Copyright (c) 2020 Dane Freeman. +# Distributed under the terms of the Modified BSD License. + +import ipyelk + + +def test_meta(): + assert hasattr(ipyelk, "__version__") diff --git a/scripts/project.py b/scripts/project.py index cad64a4d..ed83cc97 100644 --- a/scripts/project.py +++ b/scripts/project.py @@ -31,6 +31,9 @@ ) # one of: None, wheel or sdist INSTALL_ARTIFACT = os.environ.get("INSTALL_ARTIFACT") +UTEST_PROCESSES = os.environ.get( + "UTEST_PROCESSES", os.environ.get("ATEST_PROCESSES", "") +) # find root SCRIPTS = Path(__file__).parent.resolve() @@ -147,6 +150,12 @@ OK_PRETTIER = BUILD / "prettier.ok" OK_INDEX = BUILD / "index.ox" +HTMLCOV = BUILD / "htmlcov" +HTMLCOV_INDEX = HTMLCOV / "index.html" +PYTEST_COV_THRESHOLD = 17 +PYTEST_HTML = BUILD / "pytest.html" +PYTEST_XUNIT = BUILD / "pytest.xunit.xml" + # derived info PY_VERSION = re.findall( r'''__version__ = "(.*)"''', VERSION_PY.read_text(encoding="utf-8") diff --git a/scripts/reporter.py b/scripts/reporter.py new file mode 100644 index 00000000..f8b95332 --- /dev/null +++ b/scripts/reporter.py @@ -0,0 +1,53 @@ +# Copyright (c) 2020 Dane Freeman. +# Distributed under the terms of the Modified BSD License. +# +# from https://github.com/robots-from-jupyter/robotframework-jupyterlibrary +# +# Copyright (c) 2020 Robots from Jupyter +# Distributed under the terms of the Modified BSD License. + + +from datetime import datetime + +from doit.reporter import ConsoleReporter + +from . import project as P + +START = "::group::" if P.INSTALL_ARTIFACT else "" +END = "\n::endgroup::" if P.INSTALL_ARTIFACT else "" + +TIMEFMT = "%H:%M:%S" +SKIP = " " + + +class GithubActionsReporter(ConsoleReporter): + _gh_timings = {} + + def execute_task(self, task): + start = datetime.now() + title = task.title() + self._gh_timings[title] = [start] + self.outstream.write(f"""{START}[{start.strftime(TIMEFMT)}] 🦌 {title}\n""") + + def gh_outtro(self, task, emoji): + title = task.title() + start, end = self._gh_timings[title] = [ + *self._gh_timings[title], + datetime.now(), + ] + delta = end - start + sec = str(delta.seconds).rjust(7) + self.outstream.write(f"{END}\n[{sec}s] {emoji} {task.title()} {emoji}\n") + + def add_failure(self, task, exception): + super().add_failure(task, exception) + self.gh_outtro(task, "⭕") + + def add_success(self, task): + super().add_success(task) + self.gh_outtro(task, "🏁 ") + + def skip_uptodate(self, task): + self.outstream.write(f"{START}[{SKIP}] ⏩ {task.title()}{END}\n") + + skip_ignore = skip_uptodate diff --git a/scripts/utils.py b/scripts/utils.py new file mode 100644 index 00000000..40c6eeb7 --- /dev/null +++ b/scripts/utils.py @@ -0,0 +1,30 @@ +# Copyright (c) 2020 Dane Freeman. +# Distributed under the terms of the Modified BSD License. + +# from https://github.com/jupyrdf/ipyradiant/blob/master/_scripts/utils.py + +# Copyright (c) 2020 ipyradiant contributors. +# Distributed under the terms of the Modified BSD License. +import re + +RE_TIMESTAMP = r"\d{4}-\d{2}-\d{2} \d{2}:\d{2} -\d*" +RE_PYTEST_TIMESTAMP = r"on \d{2}-[^\-]+-\d{4} at \d{2}:\d{2}:\d{2}" + +PATTERNS = [RE_TIMESTAMP, RE_PYTEST_TIMESTAMP] + + +def strip_timestamps(*paths, slug="TIMESTAMP"): + """replace timestamps with a less churn-y value""" + for path in paths: + if not path.exists(): + continue + + text = original_text = path.read_text(encoding="utf-8") + + for pattern in PATTERNS: + if not re.findall(pattern, text): + continue + text = re.sub(pattern, slug, text) + + if text != original_text: + path.write_text(text) diff --git a/setup.cfg b/setup.cfg index de80eaee..a97d1a26 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,6 +39,44 @@ install_requires = where = py_src +[options.extras_require] +lint = + black + isort <5 + pyflakes + flake8 +utest = + ansi2html + hypothesis-jsonschema + pytest-asyncio + pytest-cov + pytest-html + pytest-xdist +atest = + robotframework-seleniumlibrary + robotframework-pabot + robotframework-lint +dev = + %(lint)s + %(utest)s + wheel + twine + doit + +[tool:pytest] +junit_family = xunit2 +addopts = + -vv + --ff + --pyargs ipyelk + --cov ipyelk + --cov-report term-missing:skip-covered + --cov-report html:build/htmlcov + --no-cov-on-fail + --html build/pytest.html + --self-contained-html + --junitxml build/pytest.xunit.xml + [flake8] exclude = .git,__pycache__,envs,.ipynb_checkpoints,.mypy_cache,.pytest-cache extend-ignore = E203,W503 From 2917580a9bbeac06ba4ce9d5870bf8bcca89758d Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Wed, 2 Dec 2020 16:03:30 -0500 Subject: [PATCH 12/20] removing sideeffects from XELK. separate exceptions to new file --- py_src/ipyelk/exceptions.py | 10 ++++ py_src/ipyelk/nx/transformer.py | 90 ++++++++++++++------------------- py_src/ipyelk/transform.py | 22 ++++---- 3 files changed, 61 insertions(+), 61 deletions(-) create mode 100644 py_src/ipyelk/exceptions.py diff --git a/py_src/ipyelk/exceptions.py b/py_src/ipyelk/exceptions.py new file mode 100644 index 00000000..696c6c23 --- /dev/null +++ b/py_src/ipyelk/exceptions.py @@ -0,0 +1,10 @@ +class ElkDuplicateIDError(Exception): + """Elk Ids must be unique""" + + +class ElkRegistryError(Exception): + """Transformer mark registry missing key""" + + +class ElkHierarchyError(Exception): + """[summary]""" diff --git a/py_src/ipyelk/nx/transformer.py b/py_src/ipyelk/nx/transformer.py index db958841..9a906dea 100644 --- a/py_src/ipyelk/nx/transformer.py +++ b/py_src/ipyelk/nx/transformer.py @@ -19,6 +19,7 @@ ElkRoot, ) from ..diagram.elk_text_sizer import ElkTextSizer, size_labels +from ..exceptions import ElkHierarchyError from ..transform import ( Edge, EdgeMap, @@ -36,10 +37,7 @@ class XELK(ElkTransformer): """NetworkX DiGraphs to ELK dictionary structure""" HIDDEN_ATTR = "hidden" - ELK_ROOT_ID = "root" - - _hidden_edges: Optional[EdgeMap] = None - _visible_edges: Optional[EdgeMap] = None + hoist_hidden_edges: bool = True source = T.Tuple(T.Instance(nx.Graph), T.Instance(nx.DiGraph, allow_none=True)) layouts = T.Dict() # keys: networkx nodes {ElkElements: {layout options}} @@ -88,7 +86,7 @@ def node_id(self, node: Hashable) -> str: """ g, tree = self.source if node is ElkRoot: - return "root" + return self.ELK_ROOT_ID elif node in g: return g.nodes.get(node, {}).get("_id", f"{node}") return f"{node}" @@ -120,9 +118,7 @@ def clear_cached(self): # TODO: look into ways to remove the need to have a cache like this # NOTE: this is caused by a series of side effects self.log.debug("Clearing cached elk info") - self._elk_to_item: Dict[str, Hashable] = {} - self._item_to_elk: Dict[Hashable, str] = {} - self._ports: PortMap = {} + self.clear_registry() self.closest_common_visible.cache_clear() self.closest_visible.cache_clear() @@ -134,8 +130,7 @@ async def transform(self) -> ElkNode: # TODO decide behavior for nodes that exist in the tree but not g g, tree = self.source self.clear_cached() - ports: PortMap = self._ports - self._visible_edges, self._hidden_edges = self.collect_edges() + visible_edges, hidden_edges = self.collect_edges() # Process visible networkx nodes into elknodes visible_nodes = [ @@ -143,22 +138,35 @@ async def transform(self) -> ElkNode: ] # make elknodes then connect their hierarchy - elknodes = {node: await self.make_elknode(node) for node in visible_nodes} + elknodes: NodeMap = {} + ports: PortMap = {} + for node in visible_nodes: + elknode, node_ports = await self.make_elknode(node) + for key, value in node_ports.items(): + ports[key] = value + elknodes[node] = elknode + + # make top level ElkNode and attach all others as children elknodes[ElkRoot] = top = ElkNode( id=self.ELK_ROOT_ID, children=build_hierarchy(g, tree, elknodes, self.HIDDEN_ATTR), layoutOptions=self.get_layout(ElkRoot, "parents"), ) - # TODO flag to control if slack ports and edges should be hoisted to - # closest visible ancestors - port_style = {"slack-port"} - edge_style = {"slack-edge"} + # map of original nodes to the generated elknodes + for node, elk_node in elknodes.items(): + self.register(elk_node, node) - elknodes, ports = await self.process_edges(elknodes, ports, self._visible_edges) - elknodes, ports = await self.process_edges( - elknodes, ports, self._hidden_edges, edge_style, port_style - ) + elknodes, ports = await self.process_edges(elknodes, ports, visible_edges) + # process edges with one or both original endpoints are hidden + if self.hoist_hidden_edges: + elknodes, ports = await self.process_edges( + elknodes, + ports, + hidden_edges, + edge_style={"slack-edge"}, + port_style={"slack-port"}, + ) # iterate through the port map and add ports to ElkNodes for port_id, port in ports.items(): @@ -167,25 +175,19 @@ async def transform(self) -> ElkNode: elknode = elknodes[owner] if elknode.ports is None: elknode.ports = [] - # size port labels layout = self.get_layout(owner, ElkPort) elkport.layoutOptions = merge(elkport.layoutOptions, layout) elknode.ports += [elkport] + # map of ports to the generated elkports self.register(elkport, port) - # for label in listed(elkport.labels): - # self.register(label, port) # bulk calculate label sizes await size_labels(self.text_sizer, collect_labels([top])) - # link children to parents - self._nodes = elknodes - for node, elk_node in elknodes.items(): - self.register(elk_node, node) return top - async def make_elknode(self, node) -> ElkNode: + async def make_elknode(self, node) -> Tuple[ElkNode, PortMap]: # merge layout options defined on the node data with default layout options layout = merge( self.get_node_data(node).get("layoutOptions", {}), @@ -195,8 +197,6 @@ async def make_elknode(self, node) -> ElkNode: # update port map with declared ports in the networkx node data node_ports = await self.collect_ports(node) - for key, value in node_ports.items(): - self._ports[key] = value properties = self.get_properties(node, self.get_css(node, ElkNode)) or None @@ -206,7 +206,7 @@ async def make_elknode(self, node) -> ElkNode: layoutOptions=layout, properties=properties, ) - return elk_node + return elk_node, node_ports def get_layout( self, node: Hashable, elk_type: Type[ElkGraphElement] @@ -318,19 +318,17 @@ async def process_edges( if elknode.edges is None: elknode.edges = [] if edge.source_port is not None: - source_var = (edge.source, edge.source_port) - if source_var not in ports: - port = await self.make_port( + port_id = self.port_id(edge.source, edge.source_port) + if port_id not in ports: + ports[port_id] = await self.make_port( edge.source, edge.source_port, port_css ) - ports[port.elkport.id] = port if edge.target_port is not None: - target_var = (edge.target, edge.target_port) - if target_var not in ports: - port = await self.make_port( + port_id = self.port_id(edge.target, edge.target_port) + if port_id not in ports: + ports[port_id] = await self.make_port( edge.target, edge.target_port, port_css ) - ports[port.elkport.id] = port elknode.edges += [await self.make_edge(edge, edge_css, layout_options)] return nodes, ports @@ -390,16 +388,7 @@ async def make_port( :return: [description] :rtype: ElkPort """ - # Test if elk port has already been created port_id = self.port_id(owner, port) - port = self._ports.get(port_id, None) - if port: - return port - - # TODO labels - self.get_node_data(owner).get(self.port_key, {}) - # todo ports a list or a dict? - properties = self.get_properties(port, styles) or None elk_port = ElkPort( @@ -407,10 +396,9 @@ async def make_port( height=self.port_scale, width=self.port_scale, properties=properties, + # TODO labels ) - port = Port(node=owner, elkport=elk_port) - self._ports[port_id] = port - return port + return Port(node=owner, elkport=elk_port) def get_node_data(self, node: Hashable) -> Dict: g, tree = self.source @@ -572,7 +560,7 @@ def closest_visible(self, node: Hashable): ), f"Expected only a single parent for `{node}` not {len(predecesors)}" for pred in tree.predecessors(node): return self.closest_visible(pred) - raise ValueError(f"Unable to find visible ancestor for `{node}`") + raise ElkHierarchyError(f"Unable to find visible ancestor for `{node}`") @lru_cache() def closest_common_visible(self, nodes: Tuple[Hashable]) -> Hashable: diff --git a/py_src/ipyelk/transform.py b/py_src/ipyelk/transform.py index 3d8cdadf..a994fde6 100644 --- a/py_src/ipyelk/transform.py +++ b/py_src/ipyelk/transform.py @@ -10,6 +10,7 @@ from .diagram import ElkDiagram, ElkLabel, ElkNode, ElkPort from .diagram.elk_model import ElkGraphElement from .diagram.elk_text_sizer import ElkTextSizer, size_labels +from .exceptions import ElkDuplicateIDError, ElkRegistryError from .schema import ElkSchemaValidator from .trait_types import Schema @@ -44,6 +45,7 @@ def __hash__(self): class ElkTransformer(W.Widget): """ Transform data into the form required by the ElkDiagram. """ + ELK_ROOT_ID = "root" _nodes: Optional[Dict[Hashable, ElkNode]] = None source = T.Dict() value = Schema(ElkSchemaValidator) @@ -70,7 +72,7 @@ async def transform(self) -> ElkNode: @T.default("value") def _default_value(self): - return {"id": "root"} + return {"id": self.ELK_ROOT_ID} async def _refresh(self): root_node = await self.transform() @@ -98,15 +100,17 @@ def from_id(self, element_id: str) -> Hashable: """Use the elk identifiers to find original objects""" try: return self._elk_to_item[element_id] - except KeyError: - raise ValueError(f"Element id `{element_id}` not in elk id registry.") + except KeyError as E: + raise ElkRegistryError( + f"Element id `{element_id}` not in elk id registry." + ) from E def to_id(self, item: Hashable) -> str: """Use original objects to find elk id""" try: return self._item_to_elk[item] - except KeyError: - raise ValueError(f"Item `{item}` not in elk id registry.") + except KeyError as E: + raise ElkRegistryError(f"Item `{item}` not in elk id registry.") from E def connect(self, view: ElkDiagram) -> T.link: """Connect the output value of this transformer to a diagram""" @@ -130,11 +134,9 @@ def register(self, element: Union[str, ElkGraphElement], item: Hashable): self._elk_to_item[_id] = item self._item_to_elk[item] = _id - -class ElkDuplicateIDError(Exception): - """Elk Ids must be unique""" - - pass + def clear_registry(self): + self._elk_to_item: Dict[str, Hashable] = {} + self._item_to_elk: Dict[Hashable, str] = {} def merge(d1: Optional[Dict], d2: Optional[Dict]) -> Optional[Dict]: From 0261accaa846f65de7a2a09795f5890201d1b676 Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Wed, 2 Dec 2020 19:45:35 -0500 Subject: [PATCH 13/20] new method for determining closest visible nodes when some parts of the tree are hidden --- py_src/ipyelk/nx/nx.py | 34 ++++++++++++++++++ py_src/ipyelk/nx/transformer.py | 62 ++++++++++----------------------- 2 files changed, 52 insertions(+), 44 deletions(-) diff --git a/py_src/ipyelk/nx/nx.py b/py_src/ipyelk/nx/nx.py index 1463d1da..cacb2283 100644 --- a/py_src/ipyelk/nx/nx.py +++ b/py_src/ipyelk/nx/nx.py @@ -131,3 +131,37 @@ def build_hierarchy( if not is_hidden(tree, n, HIDDEN_ATTR): roots.append(n) return [elknodes[n] for n in roots] + + +def map_visible(g:nx.Graph, tree:nx.DiGraph, attr:str)->Dict[Hashable, Hashable]: + """Build mapping of nodes to their closest visible node. + If the node is not hidden then it would map to itself. + + :param g: [description] + :type g: nx.Graph + :param tree: [description] + :type tree: nx.DiGraph + :param attr: [description] + :type attr: str + :return: [description] + :rtype: Dict[Hashable, Hashable] + """ + mapping = {} + for n in nx.algorithms.topological_sort(tree): + if n in mapping: + break # go to next node in the sorting + if not is_hidden(tree, n, attr): + mapping[n] = n + else: + predecesors = list(tree.predecessors(n)) + assert len(predecesors) <= 1 + for last_visible in predecesors: + mapping[n] = last_visible + for d in nx.algorithms.dag.descendants(tree, n): + mapping[d] = last_visible + + # creating mapping entries for those nodes not in the tree + for n in g.nodes(): + if n not in mapping: + mapping[n] = n + return mapping diff --git a/py_src/ipyelk/nx/transformer.py b/py_src/ipyelk/nx/transformer.py index 9a906dea..c2a51375 100644 --- a/py_src/ipyelk/nx/transformer.py +++ b/py_src/ipyelk/nx/transformer.py @@ -30,7 +30,7 @@ collect_labels, merge, ) -from .nx import build_hierarchy, compact, get_ports, is_hidden, lowest_common_ancestor +from .nx import build_hierarchy, compact, get_ports, is_hidden, lowest_common_ancestor, map_visible class XELK(ElkTransformer): @@ -120,7 +120,6 @@ def clear_cached(self): self.log.debug("Clearing cached elk info") self.clear_registry() self.closest_common_visible.cache_clear() - self.closest_visible.cache_clear() async def transform(self) -> ElkNode: """Generate ELK dictionary structure @@ -172,6 +171,9 @@ async def transform(self) -> ElkNode: for port_id, port in ports.items(): owner = port.node elkport = port.elkport + if owner not in elknodes: + #TODO skip generating port to begin with + break elknode = elknodes[owner] if elknode.ports is None: elknode.ports = [] @@ -493,25 +495,23 @@ def collect_edges(self) -> Tuple[EdgeMap, EdgeMap]: list ) # will index edges by nx.lowest_common_ancestor - for source, target, edge_data in g.edges(data=True): - shidden = is_hidden(tree, source, attr) - thidden = is_hidden(tree, target, attr) + closest_visible = map_visible(g, tree, attr) + for source, target, edge_data in g.edges(data=True): source_port, target_port = get_ports(edge_data) + vis_source = closest_visible[source] + vis_target = closest_visible[target] + shidden = vis_source != source + thidden = vis_target != target + owner = self.closest_common_visible((vis_source, vis_target)) if shidden or thidden: - try: - vis_source = self.closest_visible(source) - vis_target = self.closest_visible(target) - owner = self.closest_common_visible((vis_source, vis_target)) - - # create new slack ports if source or target is remapped - if vis_source != source: - source_port = (source, source_port) - if vis_target != target: - target_port = (target, target_port) - except ValueError: - continue # bail if no possible target or source + # create new slack ports if source or target is remapped + if vis_source != source: + source_port = (source, source_port) + if vis_target != target: + target_port = (target, target_port) + if vis_source != vis_target: hidden[owner].append( Edge( @@ -524,7 +524,6 @@ def collect_edges(self) -> Tuple[EdgeMap, EdgeMap]: ) ) else: - owner = self.closest_common_visible((source, target)) visible[owner].append( Edge( source=source, @@ -537,35 +536,10 @@ def collect_edges(self) -> Tuple[EdgeMap, EdgeMap]: ) return visible, hidden - @lru_cache() - def closest_visible(self, node: Hashable): - """Crawl through the given NetworkX `tree` looking for an ancestor of - `node` that is not hidden - - :param node: [description] Node to identify a visible ancestor - :type node: Hashable - :raises ValueError: [description] - :return: [description] - :rtype: [type] - """ - attr = self.HIDDEN_ATTR - g, tree = self.source - if node not in tree: - return node - if not is_hidden(tree, node, attr): - return node - predecesors = list(tree.predecessors(node)) - assert ( - len(predecesors) <= 1 - ), f"Expected only a single parent for `{node}` not {len(predecesors)}" - for pred in tree.predecessors(node): - return self.closest_visible(pred) - raise ElkHierarchyError(f"Unable to find visible ancestor for `{node}`") - @lru_cache() def closest_common_visible(self, nodes: Tuple[Hashable]) -> Hashable: g, tree = self.source if tree is None: return ElkRoot - result = lowest_common_ancestor(tree, [self.closest_visible(n) for n in nodes]) + result = lowest_common_ancestor(tree, nodes) return result From 5905dae81bb7b6d51aa55199a116408b1930886a Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Thu, 3 Dec 2020 09:23:42 -0500 Subject: [PATCH 14/20] refactoring closest common visible --- py_src/ipyelk/exceptions.py | 4 ---- py_src/ipyelk/nx/nx.py | 2 +- py_src/ipyelk/nx/transformer.py | 41 +++++++++++++++------------------ 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/py_src/ipyelk/exceptions.py b/py_src/ipyelk/exceptions.py index 696c6c23..434c361a 100644 --- a/py_src/ipyelk/exceptions.py +++ b/py_src/ipyelk/exceptions.py @@ -4,7 +4,3 @@ class ElkDuplicateIDError(Exception): class ElkRegistryError(Exception): """Transformer mark registry missing key""" - - -class ElkHierarchyError(Exception): - """[summary]""" diff --git a/py_src/ipyelk/nx/nx.py b/py_src/ipyelk/nx/nx.py index cacb2283..bcd31255 100644 --- a/py_src/ipyelk/nx/nx.py +++ b/py_src/ipyelk/nx/nx.py @@ -133,7 +133,7 @@ def build_hierarchy( return [elknodes[n] for n in roots] -def map_visible(g:nx.Graph, tree:nx.DiGraph, attr:str)->Dict[Hashable, Hashable]: +def map_visible(g: nx.Graph, tree: nx.DiGraph, attr: str) -> Dict[Hashable, Hashable]: """Build mapping of nodes to their closest visible node. If the node is not hidden then it would map to itself. diff --git a/py_src/ipyelk/nx/transformer.py b/py_src/ipyelk/nx/transformer.py index c2a51375..d7b5dc79 100644 --- a/py_src/ipyelk/nx/transformer.py +++ b/py_src/ipyelk/nx/transformer.py @@ -19,7 +19,6 @@ ElkRoot, ) from ..diagram.elk_text_sizer import ElkTextSizer, size_labels -from ..exceptions import ElkHierarchyError from ..transform import ( Edge, EdgeMap, @@ -30,7 +29,14 @@ collect_labels, merge, ) -from .nx import build_hierarchy, compact, get_ports, is_hidden, lowest_common_ancestor, map_visible +from .nx import ( + build_hierarchy, + compact, + get_ports, + is_hidden, + lowest_common_ancestor, + map_visible, +) class XELK(ElkTransformer): @@ -112,15 +118,6 @@ def edge_id(self, edge: Edge): edge.source, edge.source_port, edge.target, edge.target_port ) - def clear_cached(self): - # clear old cached info is starting at the top level transform - - # TODO: look into ways to remove the need to have a cache like this - # NOTE: this is caused by a series of side effects - self.log.debug("Clearing cached elk info") - self.clear_registry() - self.closest_common_visible.cache_clear() - async def transform(self) -> ElkNode: """Generate ELK dictionary structure :return: Root Elk node @@ -128,7 +125,7 @@ async def transform(self) -> ElkNode: """ # TODO decide behavior for nodes that exist in the tree but not g g, tree = self.source - self.clear_cached() + self.clear_registry() visible_edges, hidden_edges = self.collect_edges() # Process visible networkx nodes into elknodes @@ -172,7 +169,7 @@ async def transform(self) -> ElkNode: owner = port.node elkport = port.elkport if owner not in elknodes: - #TODO skip generating port to begin with + # TODO skip generating port to begin with break elknode = elknodes[owner] if elknode.ports is None: @@ -497,6 +494,13 @@ def collect_edges(self) -> Tuple[EdgeMap, EdgeMap]: closest_visible = map_visible(g, tree, attr) + @lru_cache() + def closest_common_visible(nodes: Tuple[Hashable]) -> Hashable: + if tree is None: + return ElkRoot + result = lowest_common_ancestor(tree, nodes) + return result + for source, target, edge_data in g.edges(data=True): source_port, target_port = get_ports(edge_data) vis_source = closest_visible[source] @@ -504,7 +508,8 @@ def collect_edges(self) -> Tuple[EdgeMap, EdgeMap]: shidden = vis_source != source thidden = vis_target != target - owner = self.closest_common_visible((vis_source, vis_target)) + owner = closest_common_visible((vis_source, vis_target)) + if shidden or thidden: # create new slack ports if source or target is remapped if vis_source != source: @@ -535,11 +540,3 @@ def collect_edges(self) -> Tuple[EdgeMap, EdgeMap]: ) ) return visible, hidden - - @lru_cache() - def closest_common_visible(self, nodes: Tuple[Hashable]) -> Hashable: - g, tree = self.source - if tree is None: - return ElkRoot - result = lowest_common_ancestor(tree, nodes) - return result From 29f01b20844d4a47f28541743e85cb76d2902066 Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Thu, 3 Dec 2020 15:05:57 -0500 Subject: [PATCH 15/20] adding missing file header info --- py_src/ipyelk/exceptions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/py_src/ipyelk/exceptions.py b/py_src/ipyelk/exceptions.py index 434c361a..2abcf2e2 100644 --- a/py_src/ipyelk/exceptions.py +++ b/py_src/ipyelk/exceptions.py @@ -1,3 +1,5 @@ +# Copyright (c) 2020 Dane Freeman. +# Distributed under the terms of the Modified BSD License. class ElkDuplicateIDError(Exception): """Elk Ids must be unique""" From 406e9b2d76ae027cfdf7dabdec390f0321e7743a Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Thu, 3 Dec 2020 15:34:09 -0500 Subject: [PATCH 16/20] test if tree is available when generating the closest visible --- py_src/ipyelk/nx/nx.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/py_src/ipyelk/nx/nx.py b/py_src/ipyelk/nx/nx.py index bcd31255..957a9ed5 100644 --- a/py_src/ipyelk/nx/nx.py +++ b/py_src/ipyelk/nx/nx.py @@ -147,18 +147,19 @@ def map_visible(g: nx.Graph, tree: nx.DiGraph, attr: str) -> Dict[Hashable, Hash :rtype: Dict[Hashable, Hashable] """ mapping = {} - for n in nx.algorithms.topological_sort(tree): - if n in mapping: - break # go to next node in the sorting - if not is_hidden(tree, n, attr): - mapping[n] = n - else: - predecesors = list(tree.predecessors(n)) - assert len(predecesors) <= 1 - for last_visible in predecesors: - mapping[n] = last_visible - for d in nx.algorithms.dag.descendants(tree, n): - mapping[d] = last_visible + if tree: + for n in nx.algorithms.topological_sort(tree): + if n in mapping: + break # go to next node in the sorting + if not is_hidden(tree, n, attr): + mapping[n] = n + else: + predecesors = list(tree.predecessors(n)) + assert len(predecesors) <= 1 + for last_visible in predecesors: + mapping[n] = last_visible + for d in nx.algorithms.dag.descendants(tree, n): + mapping[d] = last_visible # creating mapping entries for those nodes not in the tree for n in g.nodes(): From db0b6021b7449b8ee2f701f366ca17335c509efa Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Tue, 8 Dec 2020 16:50:00 -0500 Subject: [PATCH 17/20] parsing dodo.py files to build task diagram in elk --- examples/09_Doit_workflow.ipynb | 109 ++++++++++++ examples/_index.ipynb | 9 + py_src/ipyelk/contrib/__init__.py | 0 py_src/ipyelk/contrib/doit/__init__.py | 3 + py_src/ipyelk/contrib/doit/ipydoit.py | 222 +++++++++++++++++++++++++ 5 files changed, 343 insertions(+) create mode 100644 examples/09_Doit_workflow.ipynb create mode 100644 py_src/ipyelk/contrib/__init__.py create mode 100644 py_src/ipyelk/contrib/doit/__init__.py create mode 100644 py_src/ipyelk/contrib/doit/ipydoit.py diff --git a/examples/09_Doit_workflow.ipynb b/examples/09_Doit_workflow.ipynb new file mode 100644 index 00000000..9cdb839f --- /dev/null +++ b/examples/09_Doit_workflow.ipynb @@ -0,0 +1,109 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# The `DODO` Development Workflow\n", + "\n", + "> Requires `doit` to be installed in the environment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "from ipyelk.contrib.doit import DoElk" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "root = (Path() / \"..\").resolve()\n", + "doelk = DoElk(root=root)\n", + "doelk" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The DoElk object is populated with `dodo.py` tasks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "len(doelk.tasks)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The transformer graphs have nodes and edges" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "g, tree = doelk.transformer.source\n", + "print(f\"graph size \\t nodes:{len(g)} \\tedges:{len(g.edges)}\")\n", + "if tree:\n", + " print(f\"tree size \\t nodes:{len(tree)} \\tedges:{len(tree.edges)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Elk diagram value is populated so it should be trying to run elkjs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import JSON\n", + "\n", + "JSON(doelk.app.diagram.value)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.8" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/_index.ipynb b/examples/_index.ipynb index f541fff0..41528560 100644 --- a/examples/_index.ipynb +++ b/examples/_index.ipynb @@ -62,6 +62,15 @@ "- [🦌 ELK Transformer Ports 🚢](./105_transformer_ports.ipynb)\n", "- [🦌 ELK Transformer Edges 💭](./106_transformer_edges.ipynb)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Advanced Layout Options\n", + "\n", + "- [🦌 Doit workflow 🛠️](./09_Doit_workflow.ipynb)" + ] } ], "metadata": { diff --git a/py_src/ipyelk/contrib/__init__.py b/py_src/ipyelk/contrib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/py_src/ipyelk/contrib/doit/__init__.py b/py_src/ipyelk/contrib/doit/__init__.py new file mode 100644 index 00000000..31bbacc5 --- /dev/null +++ b/py_src/ipyelk/contrib/doit/__init__.py @@ -0,0 +1,3 @@ +from .ipydoit import DoElk, PyFileDoit + +__all__ = ["DoElk", "PyFileDoit"] diff --git a/py_src/ipyelk/contrib/doit/ipydoit.py b/py_src/ipyelk/contrib/doit/ipydoit.py new file mode 100644 index 00000000..046a91bf --- /dev/null +++ b/py_src/ipyelk/contrib/doit/ipydoit.py @@ -0,0 +1,222 @@ +from pathlib import Path + +import doit +import ipywidgets as W +import networkx as nx +import traitlets as T + +import ipyelk +import ipyelk.diagram.layout_options as opts +import ipyelk.nx +from ipyelk.diagram.elk_model import ElkLabel, ElkNode, ElkRoot +from ipyelk.tools import tools + + +def relative(paths, root): + """Utility function to convert a list of potentially absolute paths to be + relative to a given root + """ + for f in paths: + Path(f) + results = [] + for f in paths: + path = Path(f) + if path.is_absolute(): + path = path.relative_to(root) + results.append(str(path)) + + return results + + +class Doit(W.Widget): + doit_ = T.Instance(doit.doit_cmd.DoitMain) + tasks = W.trait_types.TypedTuple(trait=T.Instance(doit.task.Task)) + + @T.observe("doit_") + def on_doit_change(self, change): + cmds = self.doit_.get_cmds() + tasks = self.doit_.task_loader.load_tasks(cmds["list"], None) + tc = doit.control.TaskControl(tasks) + self.tasks = tuple(tc.tasks.values()) + + +class FileDoit(Doit): + path = T.Unicode("dodo.py").tag(sync=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.on_path_change(T.Bunch(new=self.path)) + + @T.observe("path") + def _on_path_change(self, change): + self.on_path_change(change) + + def on_path_change(self, change): + pass + + +class PyFileDoit(FileDoit): + def on_path_change(self, change=None): + self.doit_ = doit.doit_cmd.DoitMain( + doit.cmd_base.ModuleTaskLoader(doit.loader.get_module(self.path)) + ) + + +class DoElk(W.VBox): + app = T.Instance(ipyelk.Elk) + _doit = T.Instance(PyFileDoit) + root = T.Instance(Path) + tasks = W.trait_types.TypedTuple(trait=T.Instance(doit.task.Task)) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.layout = {"height": "100%"} + + # TODO bookeeping if doit changes + T.dlink((self._doit, "tasks"), (self, "tasks")) + + self.children = [self.app] + + @T.default("_doit") + def _default_doit(self): + doit = PyFileDoit(path=str(self.root / "dodo.py")) + return doit + + @T.observe("root") + def _update_doit(self, change=None): + self._doit.path = str(self.root / "dodo.py") + + @T.default("app") + def _default_app(self): + node_layoutopts = opts.OptionsWidget( + identifier=ElkNode, + options=[ + opts.PortLabelPlacement(), + opts.NodeSizeConstraints(), + opts.PortSide(), + opts.PortConstraints(value="FIXED_SIDE"), + ], + ) + + parents_layoutopts = opts.OptionsWidget( + identifier="parents", + options=[opts.HierarchyHandling(), opts.EdgeRouting(value="ORTHOGONAL")], + ) + label_layoutopts = opts.OptionsWidget( + identifier=ElkLabel, options=[opts.NodeLabelPlacement(horizontal="center")] + ) + root_layout = opts.OptionsWidget( + options=[ + parents_layoutopts, + node_layoutopts, + label_layoutopts, + ] + ) + + xelk = ipyelk.nx.XELK(layouts={ElkRoot: root_layout.value}) + + app = ipyelk.Elk( + transformer=xelk, + layout=dict(height="100%"), + style={ + " .sprotty rect.elknode": { + "stroke-width": "3px", + }, + " .sprotty .dodo_file > rect.elknode": { + "stroke": "rgba(15, 153, 96)", + }, + " .sprotty .dodo_task > rect.elknode": { + "stroke": "rgb(153,15,15)", + }, + }, + ) + + app.toolbar.commands = [ + tools.FitBtn(app=app), + tools.ToggleCollapsedBtn(app=app), + ] + return app + + @property + def transformer(self): + return self.app.transformer + + @T.observe("tasks") + def _update_diagram(self, change=None): + graph = nx.MultiDiGraph() + tree = nx.DiGraph() + + in_port = {opts.PortSide.identifier: opts.PortSide(value="WEST").value} + out_port = {opts.PortSide.identifier: opts.PortSide(value="EAST").value} + + tasks = self.tasks + targets = {} + for task in tasks: + for t in relative(task.targets, self.root): + if t in targets: + raise KeyError("Duplicated target") + targets[t] = task + + for t in tasks: + task_id = t.name + ports = [] + + # relative file dependencies + for f in relative(t.file_dep, self.root): + dep_id = f + port_id = f"{task_id}.{f}" + + ports.append( + { + "id": port_id, + "labels": [ + { + "id": f"{port_id}__label", + "text": "/".join(f.split("/")[-2::]), + } + ], + "layoutOptions": in_port, + } + ) + + if f in targets: + source_task = targets[f].name + graph.add_edge( + source_task, task_id, sourcePort=f, targetPort=dep_id + ) + else: + # file dependency outside of doit + graph.add_node(dep_id, properties={"cssClasses": "dodo_file"}) + graph.add_edge(dep_id, task_id, targetPort=dep_id) + + for f in relative(t.targets, self.root): + dep_id = f + port_id = f"{task_id}.{f}" + + ports.append( + { + "id": port_id, + "labels": [ + { + "id": f"{port_id}__label", + "text": "/".join(f.split("/")[-2::]), + } + ], + "layoutOptions": out_port, + } + ) + # graph.add_edge(task_id, dep_id, sourcePort=dep_id) + + graph.add_node(t.name, ports=ports, properties={"cssClasses": "dodo_task"}) + + if ":" in t.name: + namespaces = t.name.split(":") + parent = namespaces[0] + for i, lvl in enumerate(namespaces[1:]): + if parent not in graph: + graph.add_node(parent, properties={"cssClasses": "dodo_task"}) + child = ":".join([parent, lvl]) + tree.add_edge(parent, child) + parent = child + self.transformer.source = (graph, tree) + self.transformer.refresh() From 9c291ea02e7dde154af3538f817959a5c276f270 Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Tue, 8 Dec 2020 17:12:43 -0500 Subject: [PATCH 18/20] resolve environment and adding missing header info --- anaconda-project-lock.yml | 732 +++++++++++++------------ anaconda-project.yml | 1 + py_src/ipyelk/contrib/__init__.py | 4 + py_src/ipyelk/contrib/doit/__init__.py | 3 + py_src/ipyelk/contrib/doit/ipydoit.py | 3 + 5 files changed, 386 insertions(+), 357 deletions(-) diff --git a/anaconda-project-lock.yml b/anaconda-project-lock.yml index 2345bd1e..89044769 100644 --- a/anaconda-project-lock.yml +++ b/anaconda-project-lock.yml @@ -23,376 +23,394 @@ env_specs: locked: false default: locked: true - env_spec_hash: b00fef94d2a2f103b777c177b56f90dfcf7fdc44 + env_spec_hash: dd55ac5c70504129f612113e918f1ee56ed7deab platforms: - - linux-64 - - osx-64 - - win-64 + - linux-64 + - osx-64 + - win-64 packages: all: - - apipkg=1.5=py_0 - - appdirs=1.4.4=pyh9f0ad1d_0 - - async-lru=1.0.2=py_0 - - async_generator=1.10=py_0 - - attrs=20.3.0=pyhd3deb0d_0 - - backcall=0.2.0=pyh9f0ad1d_0 - - backports.functools_lru_cache=1.6.1=py_0 - - backports=1.0=py_2 - - black=20.8b1=py_1 - - bleach=3.2.1=pyh9f0ad1d_0 - - click=7.1.2=pyh9f0ad1d_0 - - colorama=0.4.4=pyh9f0ad1d_0 - - dataclasses=0.7=pyhb2cacf7_7 - - decorator=4.4.2=py_0 - - defusedxml=0.6.0=py_0 - - entrypoints=0.3=pyhd8ed1ab_1003 - - execnet=1.7.1=py_0 - - flake8=3.8.4=py_0 - - hypothesis-jsonschema=0.18.2=pyhd8ed1ab_0 - - hypothesis=5.41.4=pyhd8ed1ab_0 - - idna=2.10=pyh9f0ad1d_0 - - importlib-metadata=3.1.1=pyhd8ed1ab_0 - - importlib_metadata=3.1.1=hd8ed1ab_0 - - importnb=0.7.0=pyhd8ed1ab_0 - - iniconfig=1.1.1=pyh9f0ad1d_0 - - ipython_genutils=0.2.0=py_1 - - ipywidgets=7.5.1=pyh9f0ad1d_1 - - isort=4.3.21=py37hc8dfbb8_1 - - jinja2=2.11.2=pyh9f0ad1d_0 - - json5=0.9.5=pyh9f0ad1d_0 - - jsonschema=3.2.0=py_2 - - jupyter_client=6.1.7=py_0 - - jupyterlab=1.2.16=py_0 - - jupyterlab_pygments=0.1.2=pyh9f0ad1d_0 - - jupyterlab_server=1.2.0=py_0 - - mccabe=0.6.1=py_1 - - more-itertools=8.6.0=pyhd8ed1ab_0 - - mypy_extensions=0.4.3=py37hc8dfbb8_2 - - nbclient=0.5.1=py_0 - - nbformat=5.0.8=py_0 - - nest-asyncio=1.4.3=pyhd8ed1ab_0 - - networkx=2.5=py_0 - - packaging=20.4=pyh9f0ad1d_0 - - pandocfilters=1.4.2=py_1 - - parso=0.7.1=pyh9f0ad1d_0 - - pathspec=0.8.1=pyhd3deb0d_0 - - pickleshare=0.7.5=py_1003 - - pip=20.3=pyhd8ed1ab_0 - - pkginfo=1.6.1=pyh9f0ad1d_0 - - prometheus_client=0.9.0=pyhd3deb0d_0 - - prompt-toolkit=3.0.8=pyha770c72_0 - - py=1.9.0=pyh9f0ad1d_0 - - pycodestyle=2.6.0=pyh9f0ad1d_0 - - pycparser=2.20=pyh9f0ad1d_2 - - pyflakes=2.2.0=pyh9f0ad1d_0 - - pygments=2.7.2=py_0 - - pyopenssl=20.0.0=pyhd8ed1ab_0 - - pyparsing=2.4.7=pyh9f0ad1d_0 - - pytest-cov=2.10.1=pyh9f0ad1d_0 - - pytest-forked=1.2.0=pyh9f0ad1d_0 - - pytest-html=3.0.0=pyhd8ed1ab_0 - - pytest-metadata=1.11.0=pyhd3deb0d_0 - - pytest-xdist=2.1.0=py_0 - - python-dateutil=2.8.1=py_0 - - python_abi=3.7=1_cp37m - - pytz=2020.4=pyhd8ed1ab_0 - - readme_renderer=27.0=pyh9f0ad1d_0 - - requests-toolbelt=0.9.1=py_0 - - requests=2.25.0=pyhd3deb0d_0 - - rfc3986=1.4.0=pyh9f0ad1d_0 - - send2trash=1.5.0=py_0 - - six=1.15.0=pyh9f0ad1d_0 - - sortedcontainers=2.3.0=pyhd8ed1ab_0 - - testpath=0.4.4=py_0 - - toml=0.10.2=pyhd8ed1ab_0 - - tqdm=4.54.0=pyhd8ed1ab_0 - - traitlets=5.0.5=py_0 - - twine=3.2.0=py37hc8dfbb8_1 - - typing_extensions=3.7.4.3=py_0 - - urllib3=1.25.11=py_0 - - wcwidth=0.2.5=pyh9f0ad1d_2 - - webencodings=0.5.1=py_1 - - wheel=0.36.0=pyhd3deb0d_0 - - zipp=3.4.0=py_0 + - apipkg=1.5=py_0 + - appdirs=1.4.4=pyh9f0ad1d_0 + - async-lru=1.0.2=py_0 + - async_generator=1.10=py_0 + - attrs=20.3.0=pyhd3deb0d_0 + - backcall=0.2.0=pyh9f0ad1d_0 + - backports.functools_lru_cache=1.6.1=py_0 + - backports=1.0=py_2 + - black=20.8b1=py_1 + - bleach=3.2.1=pyh9f0ad1d_0 + - click=7.1.2=pyh9f0ad1d_0 + - cloudpickle=1.6.0=py_0 + - colorama=0.4.4=pyh9f0ad1d_0 + - decorator=4.4.2=py_0 + - defusedxml=0.6.0=py_0 + - doit=0.33.1=py37hc8dfbb8_1 + - execnet=1.7.1=py_0 + - flake8=3.8.4=py_0 + - hypothesis-jsonschema=0.18.2=pyhd8ed1ab_0 + - hypothesis=5.41.5=pyhd8ed1ab_0 + - idna=2.10=pyh9f0ad1d_0 + - importlib-metadata=3.1.1=pyhd8ed1ab_0 + - importlib_metadata=3.1.1=hd8ed1ab_0 + - importnb=0.7.0=pyhd8ed1ab_0 + - iniconfig=1.1.1=pyh9f0ad1d_0 + - ipython_genutils=0.2.0=py_1 + - ipywidgets=7.5.1=pyh9f0ad1d_1 + - isort=4.3.21=py37hc8dfbb8_1 + - jinja2=2.11.2=pyh9f0ad1d_0 + - json5=0.9.5=pyh9f0ad1d_0 + - jupyter_client=6.1.7=py_0 + - jupyterlab=1.2.16=py_0 + - jupyterlab_pygments=0.1.2=pyh9f0ad1d_0 + - jupyterlab_server=1.2.0=py_0 + - mccabe=0.6.1=py_1 + - more-itertools=8.6.0=pyhd8ed1ab_0 + - mypy_extensions=0.4.3=py37hc8dfbb8_2 + - nbclient=0.5.1=py_0 + - nbformat=5.0.8=py_0 + - nest-asyncio=1.4.3=pyhd8ed1ab_0 + - networkx=2.5=py_0 + - packaging=20.7=pyhd3deb0d_0 + - parso=0.7.1=pyh9f0ad1d_0 + - pathspec=0.8.1=pyhd3deb0d_0 + - pip=20.3.1=pyhd8ed1ab_0 + - pkginfo=1.6.1=pyh9f0ad1d_0 + - prometheus_client=0.9.0=pyhd3deb0d_0 + - prompt-toolkit=3.0.8=pyha770c72_0 + - py=1.9.0=pyh9f0ad1d_0 + - pycodestyle=2.6.0=pyh9f0ad1d_0 + - pycparser=2.20=pyh9f0ad1d_2 + - pyflakes=2.2.0=pyh9f0ad1d_0 + - pygments=2.7.3=pyhd8ed1ab_0 + - pyopenssl=20.0.0=pyhd8ed1ab_0 + - pyparsing=2.4.7=pyh9f0ad1d_0 + - pytest-cov=2.10.1=pyh9f0ad1d_0 + - pytest-html=3.1.0=pyhd8ed1ab_0 + - pytest-metadata=1.11.0=pyhd3deb0d_0 + - pytest-xdist=2.1.0=py_0 + - python-dateutil=2.8.1=py_0 + - python_abi=3.7=1_cp37m + - pytz=2020.4=pyhd8ed1ab_0 + - readme_renderer=27.0=pyh9f0ad1d_0 + - requests-toolbelt=0.9.1=py_0 + - requests=2.25.0=pyhd3deb0d_0 + - rfc3986=1.4.0=pyh9f0ad1d_0 + - send2trash=1.5.0=py_0 + - six=1.15.0=pyh9f0ad1d_0 + - sortedcontainers=2.3.0=pyhd8ed1ab_0 + - testpath=0.4.4=py_0 + - toml=0.10.2=pyhd8ed1ab_0 + - tqdm=4.54.1=pyhd8ed1ab_0 + - traitlets=5.0.5=py_0 + - twine=3.2.0=py37hc8dfbb8_1 + - typing_extensions=3.7.4.3=py_0 + - urllib3=1.25.11=py_0 + - wcwidth=0.2.5=pyh9f0ad1d_2 + - webencodings=0.5.1=py_1 + - wheel=0.36.1=pyhd3deb0d_0 + - zipp=3.4.0=py_0 unix: - - libblas=3.9.0=3_openblas - - libcblas=3.9.0=3_openblas - - liblapack=3.9.0=3_openblas - - pexpect=4.8.0=pyh9f0ad1d_2 - - ptyprocess=0.6.0=py_1001 + - libblas=3.9.0=3_openblas + - libcblas=3.9.0=3_openblas + - liblapack=3.9.0=3_openblas linux-64: - - _libgcc_mutex=0.1=conda_forge - - _openmp_mutex=4.5=1_gnu - - ansi2html=1.6.0=py37h89c1867_0 - - argon2-cffi=20.1.0=py37h4abf009_2 - - brotlipy=0.7.0=py37hb5d75c8_1001 - - ca-certificates=2020.11.8=ha878542_0 - - certifi=2020.11.8=py37h89c1867_0 - - cffi=1.14.4=py37hc58025e_1 - - chardet=3.0.4=py37he5f6b98_1008 - - cmarkgfm=0.4.2=py37h8f50634_3 - - coverage=5.3=py37h8f50634_1 - - cryptography=3.2.1=py37hc72a4ac_0 - - dbus=1.13.6=hfdff14a_1 - - docutils=0.16=py37he5f6b98_2 - - expat=2.2.9=he1b5a44_2 - - gettext=0.19.8.1=h0b5b191_1005 - - glib=2.66.3=h9c3ff4c_1 - - icu=68.1=h58526e2_0 - - importlib_resources=3.3.0=py37h89c1867_0 - - ipykernel=5.3.4=py37h888b3d9_1 - - ipython=7.19.0=py37h888b3d9_0 - - jedi=0.17.2=py37h89c1867_1 - - jeepney=0.6.0=pyhd8ed1ab_0 - - jupyter_core=4.7.0=py37h89c1867_0 - - keyring=21.5.0=py37h89c1867_0 - - ld_impl_linux-64=2.35.1=hed1e6ac_0 - - libffi=3.3=h58526e2_1 - - libgcc-ng=9.3.0=h5dbcf3e_17 - - libgfortran-ng=9.3.0=he4bcb1c_17 - - libgfortran5=9.3.0=he4bcb1c_17 - - libglib=2.66.3=h1f3bc88_1 - - libgomp=9.3.0=h5dbcf3e_17 - - libiconv=1.16=h516909a_0 - - libopenblas=0.3.12=pthreads_h4812303_1 - - libsodium=1.0.18=h36c2ea0_1 - - libstdcxx-ng=9.3.0=h2ae2ef3_17 - - libuv=1.40.0=hd18ef5c_0 - - markupsafe=1.1.1=py37hb5d75c8_2 - - mistune=0.8.4=py37h4abf009_1002 - - nbconvert=6.0.7=py37h89c1867_3 - - ncurses=6.2=h58526e2_4 - - nodejs=14.15.1=h25f6087_0 - - notebook=6.1.5=py37h89c1867_0 - - numpy=1.19.4=py37h7e9df27_1 - - openssl=1.1.1h=h516909a_0 - - pandas=1.1.4=py37h10a2094_0 - - pandoc=2.11.2=h36c2ea0_0 - - pcre=8.44=he1b5a44_0 - - pluggy=0.13.1=py37he5f6b98_3 - - pyrsistent=0.17.3=py37h4abf009_1 - - pysocks=1.7.1=py37he5f6b98_2 - - pytest-asyncio=0.14.0=py37h89c1867_0 - - pytest=6.1.2=py37h89c1867_0 - - python=3.7.8=hffdb5ce_3_cpython - - pyzmq=20.0.0=py37h5a562af_1 - - readline=8.0=he28a2e2_2 - - regex=2020.11.13=py37h4abf009_0 - - secretstorage=3.3.0=py37h89c1867_0 - - setuptools=49.6.0=py37he5f6b98_2 - - sqlite=3.34.0=h74cdb3f_0 - - terminado=0.9.1=py37h89c1867_1 - - tk=8.6.10=hed695b0_1 - - tornado=6.1=py37h4abf009_0 - - typed-ast=1.4.1=py37h4abf009_1 - - widgetsnbextension=3.5.1=py37h89c1867_4 - - xz=5.2.5=h516909a_1 - - zeromq=4.3.3=h58526e2_3 - - zlib=1.2.11=h516909a_1010 + - _libgcc_mutex=0.1=conda_forge + - _openmp_mutex=4.5=1_gnu + - ansi2html=1.6.0=py37h89c1867_0 + - argon2-cffi=20.1.0=py37h8f50634_2 + - brotlipy=0.7.0=py37hb5d75c8_1001 + - ca-certificates=2020.12.5=ha878542_0 + - certifi=2020.12.5=py37h89c1867_0 + - cffi=1.14.4=py37hc58025e_1 + - chardet=3.0.4=py37he5f6b98_1008 + - cmarkgfm=0.4.2=py37h8f50634_3 + - coverage=5.3=py37h8f50634_1 + - cryptography=3.2.1=py37hc72a4ac_0 + - dataclasses=0.7=py37_0 + - dbus=1.13.18=hb2f20db_0 + - docutils=0.16=py37he5f6b98_2 + - entrypoints=0.3=py37hc8dfbb8_1002 + - expat=2.2.10=he6710b0_2 + - gettext=0.19.8.1=h0b5b191_1005 + - glib=2.66.3=h9c3ff4c_1 + - icu=68.1=h58526e2_0 + - importlib_resources=3.3.0=py37h89c1867_0 + - ipykernel=5.3.4=py37hc6149b9_1 + - ipython=7.19.0=py37h888b3d9_0 + - jedi=0.17.2=py37hc8dfbb8_1 + - jeepney=0.6.0=pyhd8ed1ab_0 + - jsonschema=3.2.0=py37hc8dfbb8_1 + - jupyter_core=4.7.0=py37h89c1867_0 + - keyring=21.5.0=py37h89c1867_0 + - ld_impl_linux-64=2.35.1=hed1e6ac_0 + - libffi=3.3=h58526e2_2 + - libgcc-ng=9.3.0=h5dbcf3e_17 + - libgfortran-ng=7.5.0=hae1eefd_17 + - libgfortran4=7.5.0=hae1eefd_17 + - libglib=2.66.3=h1f3bc88_1 + - libgomp=9.3.0=h5dbcf3e_17 + - libiconv=1.16=h516909a_0 + - libopenblas=0.3.12=pthreads_hb3c22a3_1 + - libsodium=1.0.18=h516909a_1 + - libstdcxx-ng=9.3.0=h2ae2ef3_17 + - libuv=1.40.0=hd18ef5c_0 + - markupsafe=1.1.1=py37hb5d75c8_2 + - mistune=0.8.4=py37h8f50634_1002 + - nbconvert=6.0.7=py37h89c1867_3 + - ncurses=6.2=h58526e2_4 + - nodejs=14.15.1=h25f6087_0 + - notebook=6.1.5=py37h89c1867_0 + - numpy=1.19.4=py37h7e9df27_1 + - openssl=1.1.1h=h516909a_0 + - pandas=1.1.5=py37hdc94413_0 + - pandoc=2.11.2=h36c2ea0_0 + - pandocfilters=1.4.3=py37h06a4308_1 + - pcre=8.44=he1b5a44_0 + - pexpect=4.8.0=py37hc8dfbb8_1 + - pickleshare=0.7.5=py37hc8dfbb8_1002 + - pluggy=0.13.1=py37he5f6b98_3 + - ptyprocess=0.6.0=py37_1000 + - pyinotify=0.9.6=py37hc8dfbb8_1002 + - pyrsistent=0.17.3=py37h8f50634_1 + - pysocks=1.7.1=py37he5f6b98_2 + - pytest-asyncio=0.14.0=py37h89c1867_0 + - pytest-forked=1.3.0=py_0 + - pytest=6.1.2=py37h89c1867_0 + - python=3.7.9=h7579374_0 + - pyzmq=20.0.0=py37h5a562af_1 + - readline=8.0=he28a2e2_2 + - regex=2020.11.13=py37h4abf009_0 + - secretstorage=3.3.0=py37h89c1867_0 + - setuptools=51.0.0=py37h06a4308_2 + - sqlite=3.34.0=h74cdb3f_0 + - terminado=0.9.1=py37hc8dfbb8_1 + - tk=8.6.10=hed695b0_1 + - tornado=6.1=py37h4abf009_0 + - typed-ast=1.4.1=py37h4abf009_1 + - widgetsnbextension=3.5.1=py37hc8dfbb8_4 + - xz=5.2.5=h516909a_1 + - zeromq=4.3.3=h58526e2_3 + - zlib=1.2.11=h516909a_1010 osx-64: - - ansi2html=1.6.0=py37hf985489_0 - - appnope=0.1.2=py37hf985489_0 - - argon2-cffi=20.1.0=py37h4b544eb_2 - - brotlipy=0.7.0=py37h395d20d_1001 - - ca-certificates=2020.11.8=h033912b_0 - - certifi=2020.11.8=py37hf985489_0 - - cffi=1.14.4=py37hc5b2277_1 - - chardet=3.0.4=py37h2987424_1008 - - cmarkgfm=0.4.2=py37h60d8a13_3 - - coverage=5.3=py37h60d8a13_1 - - cryptography=3.2.1=py37h3b7a55b_0 - - docutils=0.16=py37h2987424_2 - - icu=68.1=h74dc148_0 - - importlib_resources=3.3.0=py37hf985489_0 - - ipykernel=5.3.4=py37he01cfaa_1 - - ipython=7.19.0=py37he01cfaa_0 - - jedi=0.17.2=py37hf985489_1 - - jupyter_core=4.7.0=py37hf985489_0 - - keyring=21.5.0=py37hf985489_0 - - libcxx=11.0.0=h4c3b8ed_1 - - libffi=3.3=h74dc148_1 - - libgfortran5=9.3.0=h7cc5361_13 - - libgfortran=5.0.0=h7cc5361_13 - - libopenblas=0.3.12=openmp_h54245bb_1 - - libsodium=1.0.18=hbcb3906_1 - - libuv=1.40.0=h22f3db7_0 - - llvm-openmp=11.0.0=h73239a0_1 - - markupsafe=1.1.1=py37h395d20d_2 - - mistune=0.8.4=py37h4b544eb_1002 - - nbconvert=6.0.7=py37hf985489_3 - - ncurses=6.2=h2e338ed_4 - - nodejs=14.15.1=heaeed3d_0 - - notebook=6.1.5=py37hf985489_0 - - numpy=1.19.4=py37h9ebeaaa_1 - - openssl=1.1.1h=haf1e3a3_0 - - pandas=1.1.4=py37h9b0e0a3_0 - - pandoc=2.11.2=hc929b4f_0 - - pluggy=0.13.1=py37h2987424_3 - - pyrsistent=0.17.3=py37h4b544eb_1 - - pysocks=1.7.1=py37h2987424_2 - - pytest-asyncio=0.14.0=py37hf985489_0 - - pytest=6.1.2=py37hf985489_0 - - python=3.7.8=h4f09611_3_cpython - - pyzmq=20.0.0=py37h47fd9b3_1 - - readline=8.0=h0678c8f_2 - - regex=2020.11.13=py37h4b544eb_0 - - setuptools=49.6.0=py37h2987424_2 - - sqlite=3.34.0=h17101e1_0 - - terminado=0.9.1=py37hf985489_1 - - tk=8.6.10=hb0a8c7a_1 - - tornado=6.1=py37h4b544eb_0 - - typed-ast=1.4.1=py37h4b544eb_1 - - widgetsnbextension=3.5.1=py37hf985489_4 - - xz=5.2.5=haf1e3a3_1 - - zeromq=4.3.3=h74dc148_3 - - zlib=1.2.11=h7795811_1010 + - ansi2html=1.6.0=py37hf985489_0 + - appnope=0.1.2=py37hf985489_0 + - argon2-cffi=20.1.0=py37h4b544eb_2 + - brotlipy=0.7.0=py37h395d20d_1001 + - ca-certificates=2020.12.5=h033912b_0 + - certifi=2020.12.5=py37hf985489_0 + - cffi=1.14.4=py37hc5b2277_1 + - chardet=3.0.4=py37h2987424_1008 + - cmarkgfm=0.4.2=py37h60d8a13_3 + - coverage=5.3=py37h60d8a13_1 + - cryptography=3.2.1=py37h3b7a55b_0 + - dataclasses=0.7=pyhb2cacf7_7 + - docutils=0.16=py37h2987424_2 + - entrypoints=0.3=pyhd8ed1ab_1003 + - icu=68.1=h74dc148_0 + - importlib_resources=3.3.0=py37hf985489_0 + - ipykernel=5.3.4=py37he01cfaa_1 + - ipython=7.19.0=py37he01cfaa_0 + - jedi=0.17.2=py37hf985489_1 + - jsonschema=3.2.0=py_2 + - jupyter_core=4.7.0=py37hf985489_0 + - keyring=21.5.0=py37hf985489_0 + - libcxx=11.0.0=h4c3b8ed_1 + - libffi=3.3=h046ec9c_2 + - libgfortran5=9.3.0=h7cc5361_13 + - libgfortran=5.0.0=h7cc5361_13 + - libopenblas=0.3.12=openmp_h54245bb_1 + - libsodium=1.0.18=hbcb3906_1 + - libuv=1.40.0=h22f3db7_0 + - llvm-openmp=11.0.0=h73239a0_1 + - macfsevents=0.8.1=py37h60d8a13_1001 + - markupsafe=1.1.1=py37h395d20d_2 + - mistune=0.8.4=py37h4b544eb_1002 + - nbconvert=6.0.7=py37hf985489_3 + - ncurses=6.2=h2e338ed_4 + - nodejs=14.15.1=heaeed3d_0 + - notebook=6.1.5=py37hf985489_0 + - numpy=1.19.4=py37h9ebeaaa_1 + - openssl=1.1.1h=haf1e3a3_0 + - pandas=1.1.5=py37h010c265_0 + - pandoc=2.11.2=hc929b4f_0 + - pandocfilters=1.4.2=py_1 + - pexpect=4.8.0=pyh9f0ad1d_2 + - pickleshare=0.7.5=py_1003 + - pluggy=0.13.1=py37h2987424_3 + - ptyprocess=0.6.0=py_1001 + - pyrsistent=0.17.3=py37h4b544eb_1 + - pysocks=1.7.1=py37h2987424_2 + - pytest-asyncio=0.14.0=py37hf985489_0 + - pytest-forked=1.2.0=pyh9f0ad1d_0 + - pytest=6.1.2=py37hf985489_0 + - python=3.7.8=h4f09611_3_cpython + - pyzmq=20.0.0=py37h47fd9b3_1 + - readline=8.0=h0678c8f_2 + - regex=2020.11.13=py37h4b544eb_0 + - setuptools=49.6.0=py37h2987424_2 + - sqlite=3.34.0=h17101e1_0 + - terminado=0.9.1=py37hf985489_1 + - tk=8.6.10=hb0a8c7a_1 + - tornado=6.1=py37h4b544eb_0 + - typed-ast=1.4.1=py37h4b544eb_1 + - widgetsnbextension=3.5.1=py37hf985489_4 + - xz=5.2.5=haf1e3a3_1 + - zeromq=4.3.3=h74dc148_3 + - zlib=1.2.11=h7795811_1010 win-64: - - ansi2html=1.6.0=py37h03978a9_0 - - argon2-cffi=20.1.0=py37hcc03f2d_2 - - atomicwrites=1.4.0=pyh9f0ad1d_0 - - brotlipy=0.7.0=py37h0013d47_1001 - - ca-certificates=2020.11.8=h5b45459_0 - - certifi=2020.11.8=py37h03978a9_0 - - cffi=1.14.4=py37hd8e9650_1 - - chardet=3.0.4=py37hf50a25e_1008 - - cmarkgfm=0.4.2=py37h4ab8f01_3 - - coverage=5.3=py37h4ab8f01_1 - - cryptography=3.2.1=py37hd8e9650_0 - - docutils=0.16=py37hf50a25e_2 - - importlib_resources=3.3.0=py37h03978a9_0 - - intel-openmp=2020.3=h57928b3_311 - - ipykernel=5.3.4=py37h7b7c402_1 - - ipython=7.19.0=py37heaed05f_0 - - jedi=0.17.2=py37h03978a9_1 - - jupyter_core=4.7.0=py37h03978a9_0 - - keyring=21.5.0=py37h03978a9_0 - - libblas=3.8.0=21_mkl - - libcblas=3.8.0=21_mkl - - liblapack=3.8.0=21_mkl - - libsodium=1.0.18=h8d14728_1 - - m2w64-gcc-libgfortran=5.3.0=6 - - m2w64-gcc-libs-core=5.3.0=7 - - m2w64-gcc-libs=5.3.0=7 - - m2w64-gmp=6.1.0=2 - - m2w64-libwinpthread-git=5.0.0.4634.697f757=2 - - markupsafe=1.1.1=py37h0013d47_2 - - mistune=0.8.4=py37hcc03f2d_1002 - - mkl=2020.4=hb70f87d_311 - - msys2-conda-epoch=20160418=1 - - nbconvert=6.0.7=py37h03978a9_3 - - nodejs=14.15.1=h57928b3_0 - - notebook=6.1.5=py37h03978a9_0 - - numpy=1.19.4=py37hd20adf4_1 - - openssl=1.1.1h=he774522_0 - - pandas=1.1.4=py37h08fd248_0 - - pandoc=2.11.2=h8ffe710_0 - - pluggy=0.13.1=py37hf50a25e_3 - - pyrsistent=0.17.3=py37hcc03f2d_1 - - pysocks=1.7.1=py37hf50a25e_2 - - pytest-asyncio=0.14.0=py37h03978a9_0 - - pytest=6.1.2=py37h03978a9_0 - - python=3.7.8=h7840368_3_cpython - - pywin32-ctypes=0.2.0=py37hc8dfbb8_1002 - - pywin32=228=py37h4ab8f01_0 - - pywinpty=0.5.7=py37hc8dfbb8_1 - - pyzmq=20.0.0=py37h0d95fc2_1 - - regex=2020.11.13=py37hcc03f2d_0 - - setuptools=49.6.0=py37hf50a25e_2 - - sqlite=3.34.0=h8ffe710_0 - - terminado=0.9.1=py37h03978a9_1 - - tornado=6.1=py37hcc03f2d_0 - - typed-ast=1.4.1=py37hcc03f2d_1 - - vc=14.1=h869be7e_1 - - vs2015_runtime=14.16.27012=h30e32a0_2 - - widgetsnbextension=3.5.1=py37h03978a9_4 - - win_inet_pton=1.1.0=py37hc8dfbb8_1 - - wincertstore=0.2=py37hc8dfbb8_1005 - - winpty=0.4.3=4 - - zeromq=4.3.3=h0e60522_3 + - ansi2html=1.6.0=py37h03978a9_0 + - argon2-cffi=20.1.0=py37hcc03f2d_2 + - atomicwrites=1.4.0=pyh9f0ad1d_0 + - brotlipy=0.7.0=py37h0013d47_1001 + - ca-certificates=2020.12.5=h5b45459_0 + - certifi=2020.12.5=py37h03978a9_0 + - cffi=1.14.4=py37hd8e9650_1 + - chardet=3.0.4=py37hf50a25e_1008 + - cmarkgfm=0.4.2=py37h4ab8f01_3 + - coverage=5.3=py37h4ab8f01_1 + - cryptography=3.2.1=py37hd8e9650_0 + - dataclasses=0.7=pyhb2cacf7_7 + - docutils=0.16=py37hf50a25e_2 + - entrypoints=0.3=pyhd8ed1ab_1003 + - importlib_resources=3.3.0=py37h03978a9_0 + - intel-openmp=2020.3=h57928b3_311 + - ipykernel=5.3.4=py37h7b7c402_1 + - ipython=7.19.0=py37heaed05f_0 + - jedi=0.17.2=py37h03978a9_1 + - jsonschema=3.2.0=py_2 + - jupyter_core=4.7.0=py37h03978a9_0 + - keyring=21.5.0=py37h03978a9_0 + - libblas=3.8.0=21_mkl + - libcblas=3.8.0=21_mkl + - liblapack=3.8.0=21_mkl + - libsodium=1.0.18=h8d14728_1 + - m2w64-gcc-libgfortran=5.3.0=6 + - m2w64-gcc-libs-core=5.3.0=7 + - m2w64-gcc-libs=5.3.0=7 + - m2w64-gmp=6.1.0=2 + - m2w64-libwinpthread-git=5.0.0.4634.697f757=2 + - markupsafe=1.1.1=py37h0013d47_2 + - mistune=0.8.4=py37hcc03f2d_1002 + - mkl=2020.4=hb70f87d_311 + - msys2-conda-epoch=20160418=1 + - nbconvert=6.0.7=py37h03978a9_3 + - nodejs=14.15.1=h57928b3_0 + - notebook=6.1.5=py37h03978a9_0 + - numpy=1.19.4=py37hd20adf4_1 + - openssl=1.1.1h=he774522_0 + - pandas=1.1.5=py37h08fd248_0 + - pandoc=2.11.2=h8ffe710_0 + - pandocfilters=1.4.2=py_1 + - pickleshare=0.7.5=py_1003 + - pluggy=0.13.1=py37hf50a25e_3 + - pyrsistent=0.17.3=py37hcc03f2d_1 + - pysocks=1.7.1=py37hf50a25e_2 + - pytest-asyncio=0.14.0=py37h03978a9_0 + - pytest-forked=1.2.0=pyh9f0ad1d_0 + - pytest=6.1.2=py37h03978a9_0 + - python=3.7.8=h7840368_3_cpython + - pywin32-ctypes=0.2.0=py37hc8dfbb8_1002 + - pywin32=228=py37h4ab8f01_0 + - pywinpty=0.5.7=py37hc8dfbb8_1 + - pyzmq=20.0.0=py37h0d95fc2_1 + - regex=2020.11.13=py37hcc03f2d_0 + - setuptools=49.6.0=py37hf50a25e_2 + - sqlite=3.34.0=h8ffe710_0 + - terminado=0.9.1=py37h03978a9_1 + - tornado=6.1=py37hcc03f2d_0 + - typed-ast=1.4.1=py37hcc03f2d_1 + - vc=14.1=h869be7e_1 + - vs2015_runtime=14.16.27012=h30e32a0_2 + - widgetsnbextension=3.5.1=py37h03978a9_4 + - win_inet_pton=1.1.0=py37hc8dfbb8_1 + - wincertstore=0.2=py37hc8dfbb8_1005 + - winpty=0.4.3=4 + - zeromq=4.3.3=h0e60522_3 atest: locked: true env_spec_hash: a3cbc653618884c4eb2c0cda4abc326eac9894ad platforms: - - linux-64 - - osx-64 - - win-64 + - linux-64 + - osx-64 + - win-64 packages: all: - - idna=2.10=pyh9f0ad1d_0 - - pip=20.3=pyhd8ed1ab_0 - - pycparser=2.20=pyh9f0ad1d_2 - - pyopenssl=20.0.0=pyhd8ed1ab_0 - - python_abi=3.7=1_cp37m - - robotframework-lint=1.1=pyh9f0ad1d_0 - - robotframework-pabot=1.10.0=py_0 - - robotframework-pythonlibcore=2.1.0=pyhd8ed1ab_1 - - robotframework-seleniumlibrary=4.5.0=pyh9f0ad1d_0 - - robotframework=3.2.2=pyh9f0ad1d_0 - - six=1.15.0=pyh9f0ad1d_0 - - urllib3=1.26.2=pyhd8ed1ab_0 - - wheel=0.36.0=pyhd3deb0d_0 + - idna=2.10=pyh9f0ad1d_0 + - pip=20.3=pyhd8ed1ab_0 + - pycparser=2.20=pyh9f0ad1d_2 + - pyopenssl=20.0.0=pyhd8ed1ab_0 + - python_abi=3.7=1_cp37m + - robotframework-lint=1.1=pyh9f0ad1d_0 + - robotframework-pabot=1.10.0=py_0 + - robotframework-pythonlibcore=2.1.0=pyhd8ed1ab_1 + - robotframework-seleniumlibrary=4.5.0=pyh9f0ad1d_0 + - robotframework=3.2.2=pyh9f0ad1d_0 + - six=1.15.0=pyh9f0ad1d_0 + - urllib3=1.26.2=pyhd8ed1ab_0 + - wheel=0.36.0=pyhd3deb0d_0 linux-64: - - _libgcc_mutex=0.1=conda_forge - - _openmp_mutex=4.5=1_gnu - - brotlipy=0.7.0=py37hb5d75c8_1001 - - ca-certificates=2020.11.8=ha878542_0 - - certifi=2020.11.8=py37h89c1867_0 - - cffi=1.14.4=py37hc58025e_1 - - cryptography=3.2.1=py37hc72a4ac_0 - - firefox=83.0=h58526e2_0 - - geckodriver=0.28.0=h58526e2_0 - - ld_impl_linux-64=2.35.1=hed1e6ac_0 - - libffi=3.3=h58526e2_1 - - libgcc-ng=9.3.0=h5dbcf3e_17 - - libgomp=9.3.0=h5dbcf3e_17 - - libstdcxx-ng=9.3.0=h2ae2ef3_17 - - ncurses=6.2=h58526e2_4 - - openssl=1.1.1h=h516909a_0 - - pysocks=1.7.1=py37he5f6b98_2 - - python=3.7.8=hffdb5ce_3_cpython - - readline=8.0=he28a2e2_2 - - selenium=3.141.0=py37h8f50634_1002 - - setuptools=49.6.0=py37he5f6b98_2 - - sqlite=3.34.0=h74cdb3f_0 - - tk=8.6.10=hed695b0_1 - - xz=5.2.5=h516909a_1 - - zlib=1.2.11=h516909a_1010 + - _libgcc_mutex=0.1=conda_forge + - _openmp_mutex=4.5=1_gnu + - brotlipy=0.7.0=py37hb5d75c8_1001 + - ca-certificates=2020.11.8=ha878542_0 + - certifi=2020.11.8=py37h89c1867_0 + - cffi=1.14.4=py37hc58025e_1 + - cryptography=3.2.1=py37hc72a4ac_0 + - firefox=83.0=h58526e2_0 + - geckodriver=0.28.0=h58526e2_0 + - ld_impl_linux-64=2.35.1=hed1e6ac_0 + - libffi=3.3=h58526e2_1 + - libgcc-ng=9.3.0=h5dbcf3e_17 + - libgomp=9.3.0=h5dbcf3e_17 + - libstdcxx-ng=9.3.0=h2ae2ef3_17 + - ncurses=6.2=h58526e2_4 + - openssl=1.1.1h=h516909a_0 + - pysocks=1.7.1=py37he5f6b98_2 + - python=3.7.8=hffdb5ce_3_cpython + - readline=8.0=he28a2e2_2 + - selenium=3.141.0=py37h8f50634_1002 + - setuptools=49.6.0=py37he5f6b98_2 + - sqlite=3.34.0=h74cdb3f_0 + - tk=8.6.10=hed695b0_1 + - xz=5.2.5=h516909a_1 + - zlib=1.2.11=h516909a_1010 osx-64: - - brotlipy=0.7.0=py37h395d20d_1001 - - ca-certificates=2020.11.8=h033912b_0 - - certifi=2020.11.8=py37hf985489_0 - - cffi=1.14.4=py37hc5b2277_1 - - cryptography=3.2.1=py37h3b7a55b_0 - - firefox=83.0=h74dc148_0 - - geckodriver=0.28.0=h2e338ed_0 - - libcxx=11.0.0=h4c3b8ed_1 - - libffi=3.3=h74dc148_1 - - ncurses=6.2=h2e338ed_4 - - openssl=1.1.1h=haf1e3a3_0 - - pysocks=1.7.1=py37h2987424_2 - - python=3.7.8=h4f09611_3_cpython - - readline=8.0=h0678c8f_2 - - selenium=3.141.0=py37h60d8a13_1002 - - setuptools=49.6.0=py37h2987424_2 - - sqlite=3.34.0=h17101e1_0 - - tk=8.6.10=hb0a8c7a_1 - - xz=5.2.5=haf1e3a3_1 - - zlib=1.2.11=h7795811_1010 + - brotlipy=0.7.0=py37h395d20d_1001 + - ca-certificates=2020.11.8=h033912b_0 + - certifi=2020.11.8=py37hf985489_0 + - cffi=1.14.4=py37hc5b2277_1 + - cryptography=3.2.1=py37h3b7a55b_0 + - firefox=83.0=h74dc148_0 + - geckodriver=0.28.0=h2e338ed_0 + - libcxx=11.0.0=h4c3b8ed_1 + - libffi=3.3=h74dc148_1 + - ncurses=6.2=h2e338ed_4 + - openssl=1.1.1h=haf1e3a3_0 + - pysocks=1.7.1=py37h2987424_2 + - python=3.7.8=h4f09611_3_cpython + - readline=8.0=h0678c8f_2 + - selenium=3.141.0=py37h60d8a13_1002 + - setuptools=49.6.0=py37h2987424_2 + - sqlite=3.34.0=h17101e1_0 + - tk=8.6.10=hb0a8c7a_1 + - xz=5.2.5=haf1e3a3_1 + - zlib=1.2.11=h7795811_1010 win-64: - - brotlipy=0.7.0=py37h0013d47_1001 - - ca-certificates=2020.11.8=h5b45459_0 - - certifi=2020.11.8=py37h03978a9_0 - - cffi=1.14.4=py37hd8e9650_1 - - cryptography=3.2.1=py37hd8e9650_0 - - firefox=83.0=h0e60522_0 - - geckodriver=0.28.0=h39d44d4_0 - - openssl=1.1.1h=he774522_0 - - pysocks=1.7.1=py37hf50a25e_2 - - python=3.7.8=h7840368_3_cpython - - selenium=3.141.0=py37h4ab8f01_1002 - - setuptools=49.6.0=py37hf50a25e_2 - - sqlite=3.34.0=h8ffe710_0 - - vc=14.1=h869be7e_1 - - vs2015_runtime=14.16.27012=h30e32a0_2 - - win_inet_pton=1.1.0=py37hc8dfbb8_1 - - wincertstore=0.2=py37hc8dfbb8_1005 + - brotlipy=0.7.0=py37h0013d47_1001 + - ca-certificates=2020.11.8=h5b45459_0 + - certifi=2020.11.8=py37h03978a9_0 + - cffi=1.14.4=py37hd8e9650_1 + - cryptography=3.2.1=py37hd8e9650_0 + - firefox=83.0=h0e60522_0 + - geckodriver=0.28.0=h39d44d4_0 + - openssl=1.1.1h=he774522_0 + - pysocks=1.7.1=py37hf50a25e_2 + - python=3.7.8=h7840368_3_cpython + - selenium=3.141.0=py37h4ab8f01_1002 + - setuptools=49.6.0=py37hf50a25e_2 + - sqlite=3.34.0=h8ffe710_0 + - vc=14.1=h869be7e_1 + - vs2015_runtime=14.16.27012=h30e32a0_2 + - win_inet_pton=1.1.0=py37hc8dfbb8_1 + - wincertstore=0.2=py37hc8dfbb8_1005 diff --git a/anaconda-project.yml b/anaconda-project.yml index 355023ae..36abb4a9 100644 --- a/anaconda-project.yml +++ b/anaconda-project.yml @@ -44,6 +44,7 @@ env_specs: - python >=3.7,<3.8.0a0 - twine - wheel + - doit ipyelk: description: binder environment diff --git a/py_src/ipyelk/contrib/__init__.py b/py_src/ipyelk/contrib/__init__.py index e69de29b..e8e2bac4 100644 --- a/py_src/ipyelk/contrib/__init__.py +++ b/py_src/ipyelk/contrib/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2020 Dane Freeman. +# Distributed under the terms of the Modified BSD License. +# Copyright (c) 2020 Dane Freeman. +# Distributed under the terms of the Modified BSD License. diff --git a/py_src/ipyelk/contrib/doit/__init__.py b/py_src/ipyelk/contrib/doit/__init__.py index 31bbacc5..79fb46e4 100644 --- a/py_src/ipyelk/contrib/doit/__init__.py +++ b/py_src/ipyelk/contrib/doit/__init__.py @@ -1,3 +1,6 @@ +# Copyright (c) 2020 Dane Freeman. +# Distributed under the terms of the Modified BSD License. + from .ipydoit import DoElk, PyFileDoit __all__ = ["DoElk", "PyFileDoit"] diff --git a/py_src/ipyelk/contrib/doit/ipydoit.py b/py_src/ipyelk/contrib/doit/ipydoit.py index 046a91bf..eb7b9cd4 100644 --- a/py_src/ipyelk/contrib/doit/ipydoit.py +++ b/py_src/ipyelk/contrib/doit/ipydoit.py @@ -1,3 +1,6 @@ +# Copyright (c) 2020 Dane Freeman. +# Distributed under the terms of the Modified BSD License. + from pathlib import Path import doit From 1486ce6fb7fd2cbd52faba58813cb9168eafda12 Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Tue, 8 Dec 2020 17:19:57 -0500 Subject: [PATCH 19/20] resolving environment after merge --- anaconda-project-lock.yml | 190 +++++++++++++++++++------------------- 1 file changed, 93 insertions(+), 97 deletions(-) diff --git a/anaconda-project-lock.yml b/anaconda-project-lock.yml index f7ab3ac0..a65cc756 100644 --- a/anaconda-project-lock.yml +++ b/anaconda-project-lock.yml @@ -30,90 +30,86 @@ env_specs: - win-64 packages: all: - - apipkg=1.5=py_0 - - appdirs=1.4.4=pyh9f0ad1d_0 - - async-lru=1.0.2=py_0 - - async_generator=1.10=py_0 - - attrs=20.3.0=pyhd3deb0d_0 - - backcall=0.2.0=pyh9f0ad1d_0 - - backports.functools_lru_cache=1.6.1=py_0 - - backports=1.0=py_2 - - black=20.8b1=py_1 - - bleach=3.2.1=pyh9f0ad1d_0 - - click=7.1.2=pyh9f0ad1d_0 - - colorama=0.4.4=pyh9f0ad1d_0 - - dataclasses=0.7=pyhb2cacf7_7 - - decorator=4.4.2=py_0 - - defusedxml=0.6.0=py_0 - - entrypoints=0.3=pyhd8ed1ab_1003 - - execnet=1.7.1=py_0 - - flake8=3.8.4=py_0 - - hypothesis-jsonschema=0.18.2=pyhd8ed1ab_0 - - hypothesis=5.41.4=pyhd8ed1ab_0 - - idna=2.10=pyh9f0ad1d_0 - - importlib-metadata=3.1.1=pyhd8ed1ab_0 - - importlib_metadata=3.1.1=hd8ed1ab_0 - - importnb=0.7.0=pyhd8ed1ab_0 - - iniconfig=1.1.1=pyh9f0ad1d_0 - - ipython_genutils=0.2.0=py_1 - - ipywidgets=7.5.1=pyh9f0ad1d_1 - - isort=4.3.21=py37hc8dfbb8_1 - - jinja2=2.11.2=pyh9f0ad1d_0 - - json5=0.9.5=pyh9f0ad1d_0 - - jsonschema=3.2.0=py_2 - - jupyter_client=6.1.7=py_0 - - jupyterlab=1.2.16=py_0 - - jupyterlab_pygments=0.1.2=pyh9f0ad1d_0 - - jupyterlab_server=1.2.0=py_0 - - mccabe=0.6.1=py_1 - - more-itertools=8.6.0=pyhd8ed1ab_0 - - mypy_extensions=0.4.3=py37hc8dfbb8_2 - - nbclient=0.5.1=py_0 - - nbformat=5.0.8=py_0 - - nest-asyncio=1.4.3=pyhd8ed1ab_0 - - networkx=2.5=py_0 - - packaging=20.7=pyhd3deb0d_0 - - pandocfilters=1.4.2=py_1 - - parso=0.7.1=pyh9f0ad1d_0 - - pathspec=0.8.1=pyhd3deb0d_0 - - pickleshare=0.7.5=py_1003 - - pip=20.3.1=pyhd8ed1ab_0 - - pkginfo=1.6.1=pyh9f0ad1d_0 - - prometheus_client=0.9.0=pyhd3deb0d_0 - - prompt-toolkit=3.0.8=pyha770c72_0 - - py=1.9.0=pyh9f0ad1d_0 - - pycodestyle=2.6.0=pyh9f0ad1d_0 - - pycparser=2.20=pyh9f0ad1d_2 - - pyflakes=2.2.0=pyh9f0ad1d_0 - - pygments=2.7.2=py_0 - - pyopenssl=20.0.0=pyhd8ed1ab_0 - - pyparsing=2.4.7=pyh9f0ad1d_0 - - pytest-cov=2.10.1=pyh9f0ad1d_0 - - pytest-forked=1.2.0=pyh9f0ad1d_0 - - pytest-html=3.1.0=pyhd8ed1ab_0 - - pytest-metadata=1.11.0=pyhd3deb0d_0 - - pytest-xdist=2.1.0=py_0 - - python-dateutil=2.8.1=py_0 - - python_abi=3.7=1_cp37m - - pytz=2020.4=pyhd8ed1ab_0 - - readme_renderer=27.0=pyh9f0ad1d_0 - - requests-toolbelt=0.9.1=py_0 - - requests=2.25.0=pyhd3deb0d_0 - - rfc3986=1.4.0=pyh9f0ad1d_0 - - send2trash=1.5.0=py_0 - - six=1.15.0=pyh9f0ad1d_0 - - sortedcontainers=2.3.0=pyhd8ed1ab_0 - - testpath=0.4.4=py_0 - - toml=0.10.2=pyhd8ed1ab_0 - - tqdm=4.54.0=pyhd8ed1ab_0 - - traitlets=5.0.5=py_0 - - twine=3.2.0=py37hc8dfbb8_1 - - typing_extensions=3.7.4.3=py_0 - - urllib3=1.25.11=py_0 - - wcwidth=0.2.5=pyh9f0ad1d_2 - - webencodings=0.5.1=py_1 - - wheel=0.36.1=pyhd3deb0d_0 - - zipp=3.4.0=py_0 + - apipkg=1.5=py_0 + - appdirs=1.4.4=pyh9f0ad1d_0 + - async-lru=1.0.2=py_0 + - async_generator=1.10=py_0 + - attrs=20.3.0=pyhd3deb0d_0 + - backcall=0.2.0=pyh9f0ad1d_0 + - backports.functools_lru_cache=1.6.1=py_0 + - backports=1.0=py_2 + - black=20.8b1=py_1 + - bleach=3.2.1=pyh9f0ad1d_0 + - click=7.1.2=pyh9f0ad1d_0 + - cloudpickle=1.6.0=py_0 + - colorama=0.4.4=pyh9f0ad1d_0 + - decorator=4.4.2=py_0 + - defusedxml=0.6.0=py_0 + - doit=0.33.1=py37hc8dfbb8_1 + - execnet=1.7.1=py_0 + - flake8=3.8.4=py_0 + - hypothesis-jsonschema=0.18.2=pyhd8ed1ab_0 + - hypothesis=5.41.5=pyhd8ed1ab_0 + - idna=2.10=pyh9f0ad1d_0 + - importlib-metadata=3.1.1=pyhd8ed1ab_0 + - importlib_metadata=3.1.1=hd8ed1ab_0 + - importnb=0.7.0=pyhd8ed1ab_0 + - iniconfig=1.1.1=pyh9f0ad1d_0 + - ipython_genutils=0.2.0=py_1 + - ipywidgets=7.5.1=pyh9f0ad1d_1 + - isort=4.3.21=py37hc8dfbb8_1 + - jinja2=2.11.2=pyh9f0ad1d_0 + - json5=0.9.5=pyh9f0ad1d_0 + - jupyter_client=6.1.7=py_0 + - jupyterlab=1.2.16=py_0 + - jupyterlab_pygments=0.1.2=pyh9f0ad1d_0 + - jupyterlab_server=1.2.0=py_0 + - mccabe=0.6.1=py_1 + - more-itertools=8.6.0=pyhd8ed1ab_0 + - mypy_extensions=0.4.3=py37hc8dfbb8_2 + - nbclient=0.5.1=py_0 + - nbformat=5.0.8=py_0 + - nest-asyncio=1.4.3=pyhd8ed1ab_0 + - networkx=2.5=py_0 + - packaging=20.7=pyhd3deb0d_0 + - parso=0.7.1=pyh9f0ad1d_0 + - pathspec=0.8.1=pyhd3deb0d_0 + - pip=20.3.1=pyhd8ed1ab_0 + - pkginfo=1.6.1=pyh9f0ad1d_0 + - prometheus_client=0.9.0=pyhd3deb0d_0 + - prompt-toolkit=3.0.8=pyha770c72_0 + - py=1.9.0=pyh9f0ad1d_0 + - pycodestyle=2.6.0=pyh9f0ad1d_0 + - pycparser=2.20=pyh9f0ad1d_2 + - pyflakes=2.2.0=pyh9f0ad1d_0 + - pygments=2.7.3=pyhd8ed1ab_0 + - pyopenssl=20.0.0=pyhd8ed1ab_0 + - pyparsing=2.4.7=pyh9f0ad1d_0 + - pytest-cov=2.10.1=pyh9f0ad1d_0 + - pytest-html=3.1.0=pyhd8ed1ab_0 + - pytest-metadata=1.11.0=pyhd3deb0d_0 + - pytest-xdist=2.1.0=py_0 + - python-dateutil=2.8.1=py_0 + - python_abi=3.7=1_cp37m + - pytz=2020.4=pyhd8ed1ab_0 + - readme_renderer=27.0=pyh9f0ad1d_0 + - requests-toolbelt=0.9.1=py_0 + - requests=2.25.0=pyhd3deb0d_0 + - rfc3986=1.4.0=pyh9f0ad1d_0 + - send2trash=1.5.0=py_0 + - six=1.15.0=pyh9f0ad1d_0 + - sortedcontainers=2.3.0=pyhd8ed1ab_0 + - testpath=0.4.4=py_0 + - toml=0.10.2=pyhd8ed1ab_0 + - tqdm=4.54.1=pyhd8ed1ab_0 + - traitlets=5.0.5=py_0 + - twine=3.2.0=py37hc8dfbb8_1 + - typing_extensions=3.7.4.3=py_0 + - urllib3=1.25.11=py_0 + - wcwidth=0.2.5=pyh9f0ad1d_2 + - webencodings=0.5.1=py_1 + - wheel=0.36.1=pyhd3deb0d_0 + - zipp=3.4.0=py_0 unix: - libblas=3.9.0=3_openblas - libcblas=3.9.0=3_openblas @@ -340,19 +336,19 @@ env_specs: - win-64 packages: all: - - idna=2.10=pyh9f0ad1d_0 - - pip=20.3.1=pyhd8ed1ab_0 - - pycparser=2.20=pyh9f0ad1d_2 - - pyopenssl=20.0.0=pyhd8ed1ab_0 - - python_abi=3.7=1_cp37m - - robotframework-lint=1.1=pyh9f0ad1d_0 - - robotframework-pabot=1.10.0=py_0 - - robotframework-pythonlibcore=2.1.0=pyhd8ed1ab_1 - - robotframework-seleniumlibrary=4.5.0=pyh9f0ad1d_0 - - robotframework=3.2.2=pyh9f0ad1d_0 - - six=1.15.0=pyh9f0ad1d_0 - - urllib3=1.26.2=pyhd8ed1ab_0 - - wheel=0.36.1=pyhd3deb0d_0 + - idna=2.10=pyh9f0ad1d_0 + - pip=20.3.1=pyhd8ed1ab_0 + - pycparser=2.20=pyh9f0ad1d_2 + - pyopenssl=20.0.0=pyhd8ed1ab_0 + - python_abi=3.7=1_cp37m + - robotframework-lint=1.1=pyh9f0ad1d_0 + - robotframework-pabot=1.10.0=py_0 + - robotframework-pythonlibcore=2.1.0=pyhd8ed1ab_1 + - robotframework-seleniumlibrary=4.5.0=pyh9f0ad1d_0 + - robotframework=3.2.2=pyh9f0ad1d_0 + - six=1.15.0=pyh9f0ad1d_0 + - urllib3=1.26.2=pyhd8ed1ab_0 + - wheel=0.36.1=pyhd3deb0d_0 linux-64: - _libgcc_mutex=0.1=conda_forge - _openmp_mutex=4.5=1_gnu From e6d52d617931fc334758755b76d04f4a5be4f79a Mon Sep 17 00:00:00 2001 From: Dane Freeman Date: Wed, 9 Dec 2020 08:13:56 -0500 Subject: [PATCH 20/20] formatting --- anaconda-project-lock.yml | 748 +++++++++++++------------- dodo.py | 1 + py_src/ipyelk/contrib/doit/ipydoit.py | 4 +- py_src/ipyelk/nx/nx.py | 19 + 4 files changed, 397 insertions(+), 375 deletions(-) diff --git a/anaconda-project-lock.yml b/anaconda-project-lock.yml index a65cc756..e2c15ce5 100644 --- a/anaconda-project-lock.yml +++ b/anaconda-project-lock.yml @@ -25,392 +25,392 @@ env_specs: locked: true env_spec_hash: dd55ac5c70504129f612113e918f1ee56ed7deab platforms: - - linux-64 - - osx-64 - - win-64 + - linux-64 + - osx-64 + - win-64 packages: all: - - apipkg=1.5=py_0 - - appdirs=1.4.4=pyh9f0ad1d_0 - - async-lru=1.0.2=py_0 - - async_generator=1.10=py_0 - - attrs=20.3.0=pyhd3deb0d_0 - - backcall=0.2.0=pyh9f0ad1d_0 - - backports.functools_lru_cache=1.6.1=py_0 - - backports=1.0=py_2 - - black=20.8b1=py_1 - - bleach=3.2.1=pyh9f0ad1d_0 - - click=7.1.2=pyh9f0ad1d_0 - - cloudpickle=1.6.0=py_0 - - colorama=0.4.4=pyh9f0ad1d_0 - - decorator=4.4.2=py_0 - - defusedxml=0.6.0=py_0 - - doit=0.33.1=py37hc8dfbb8_1 - - execnet=1.7.1=py_0 - - flake8=3.8.4=py_0 - - hypothesis-jsonschema=0.18.2=pyhd8ed1ab_0 - - hypothesis=5.41.5=pyhd8ed1ab_0 - - idna=2.10=pyh9f0ad1d_0 - - importlib-metadata=3.1.1=pyhd8ed1ab_0 - - importlib_metadata=3.1.1=hd8ed1ab_0 - - importnb=0.7.0=pyhd8ed1ab_0 - - iniconfig=1.1.1=pyh9f0ad1d_0 - - ipython_genutils=0.2.0=py_1 - - ipywidgets=7.5.1=pyh9f0ad1d_1 - - isort=4.3.21=py37hc8dfbb8_1 - - jinja2=2.11.2=pyh9f0ad1d_0 - - json5=0.9.5=pyh9f0ad1d_0 - - jupyter_client=6.1.7=py_0 - - jupyterlab=1.2.16=py_0 - - jupyterlab_pygments=0.1.2=pyh9f0ad1d_0 - - jupyterlab_server=1.2.0=py_0 - - mccabe=0.6.1=py_1 - - more-itertools=8.6.0=pyhd8ed1ab_0 - - mypy_extensions=0.4.3=py37hc8dfbb8_2 - - nbclient=0.5.1=py_0 - - nbformat=5.0.8=py_0 - - nest-asyncio=1.4.3=pyhd8ed1ab_0 - - networkx=2.5=py_0 - - packaging=20.7=pyhd3deb0d_0 - - parso=0.7.1=pyh9f0ad1d_0 - - pathspec=0.8.1=pyhd3deb0d_0 - - pip=20.3.1=pyhd8ed1ab_0 - - pkginfo=1.6.1=pyh9f0ad1d_0 - - prometheus_client=0.9.0=pyhd3deb0d_0 - - prompt-toolkit=3.0.8=pyha770c72_0 - - py=1.9.0=pyh9f0ad1d_0 - - pycodestyle=2.6.0=pyh9f0ad1d_0 - - pycparser=2.20=pyh9f0ad1d_2 - - pyflakes=2.2.0=pyh9f0ad1d_0 - - pygments=2.7.3=pyhd8ed1ab_0 - - pyopenssl=20.0.0=pyhd8ed1ab_0 - - pyparsing=2.4.7=pyh9f0ad1d_0 - - pytest-cov=2.10.1=pyh9f0ad1d_0 - - pytest-html=3.1.0=pyhd8ed1ab_0 - - pytest-metadata=1.11.0=pyhd3deb0d_0 - - pytest-xdist=2.1.0=py_0 - - python-dateutil=2.8.1=py_0 - - python_abi=3.7=1_cp37m - - pytz=2020.4=pyhd8ed1ab_0 - - readme_renderer=27.0=pyh9f0ad1d_0 - - requests-toolbelt=0.9.1=py_0 - - requests=2.25.0=pyhd3deb0d_0 - - rfc3986=1.4.0=pyh9f0ad1d_0 - - send2trash=1.5.0=py_0 - - six=1.15.0=pyh9f0ad1d_0 - - sortedcontainers=2.3.0=pyhd8ed1ab_0 - - testpath=0.4.4=py_0 - - toml=0.10.2=pyhd8ed1ab_0 - - tqdm=4.54.1=pyhd8ed1ab_0 - - traitlets=5.0.5=py_0 - - twine=3.2.0=py37hc8dfbb8_1 - - typing_extensions=3.7.4.3=py_0 - - urllib3=1.25.11=py_0 - - wcwidth=0.2.5=pyh9f0ad1d_2 - - webencodings=0.5.1=py_1 - - wheel=0.36.1=pyhd3deb0d_0 - - zipp=3.4.0=py_0 + - apipkg=1.5=py_0 + - appdirs=1.4.4=pyh9f0ad1d_0 + - async-lru=1.0.2=py_0 + - async_generator=1.10=py_0 + - attrs=20.3.0=pyhd3deb0d_0 + - backcall=0.2.0=pyh9f0ad1d_0 + - backports.functools_lru_cache=1.6.1=py_0 + - backports=1.0=py_2 + - black=20.8b1=py_1 + - bleach=3.2.1=pyh9f0ad1d_0 + - click=7.1.2=pyh9f0ad1d_0 + - cloudpickle=1.6.0=py_0 + - colorama=0.4.4=pyh9f0ad1d_0 + - decorator=4.4.2=py_0 + - defusedxml=0.6.0=py_0 + - doit=0.33.1=py37hc8dfbb8_1 + - execnet=1.7.1=py_0 + - flake8=3.8.4=py_0 + - hypothesis-jsonschema=0.18.2=pyhd8ed1ab_0 + - hypothesis=5.41.5=pyhd8ed1ab_0 + - idna=2.10=pyh9f0ad1d_0 + - importlib-metadata=3.1.1=pyhd8ed1ab_0 + - importlib_metadata=3.1.1=hd8ed1ab_0 + - importnb=0.7.0=pyhd8ed1ab_0 + - iniconfig=1.1.1=pyh9f0ad1d_0 + - ipython_genutils=0.2.0=py_1 + - ipywidgets=7.5.1=pyh9f0ad1d_1 + - isort=4.3.21=py37hc8dfbb8_1 + - jinja2=2.11.2=pyh9f0ad1d_0 + - json5=0.9.5=pyh9f0ad1d_0 + - jupyter_client=6.1.7=py_0 + - jupyterlab=1.2.16=py_0 + - jupyterlab_pygments=0.1.2=pyh9f0ad1d_0 + - jupyterlab_server=1.2.0=py_0 + - mccabe=0.6.1=py_1 + - more-itertools=8.6.0=pyhd8ed1ab_0 + - mypy_extensions=0.4.3=py37hc8dfbb8_2 + - nbclient=0.5.1=py_0 + - nbformat=5.0.8=py_0 + - nest-asyncio=1.4.3=pyhd8ed1ab_0 + - networkx=2.5=py_0 + - packaging=20.7=pyhd3deb0d_0 + - parso=0.7.1=pyh9f0ad1d_0 + - pathspec=0.8.1=pyhd3deb0d_0 + - pip=20.3.1=pyhd8ed1ab_0 + - pkginfo=1.6.1=pyh9f0ad1d_0 + - prometheus_client=0.9.0=pyhd3deb0d_0 + - prompt-toolkit=3.0.8=pyha770c72_0 + - py=1.9.0=pyh9f0ad1d_0 + - pycodestyle=2.6.0=pyh9f0ad1d_0 + - pycparser=2.20=pyh9f0ad1d_2 + - pyflakes=2.2.0=pyh9f0ad1d_0 + - pygments=2.7.3=pyhd8ed1ab_0 + - pyopenssl=20.0.0=pyhd8ed1ab_0 + - pyparsing=2.4.7=pyh9f0ad1d_0 + - pytest-cov=2.10.1=pyh9f0ad1d_0 + - pytest-html=3.1.0=pyhd8ed1ab_0 + - pytest-metadata=1.11.0=pyhd3deb0d_0 + - pytest-xdist=2.1.0=py_0 + - python-dateutil=2.8.1=py_0 + - python_abi=3.7=1_cp37m + - pytz=2020.4=pyhd8ed1ab_0 + - readme_renderer=27.0=pyh9f0ad1d_0 + - requests-toolbelt=0.9.1=py_0 + - requests=2.25.0=pyhd3deb0d_0 + - rfc3986=1.4.0=pyh9f0ad1d_0 + - send2trash=1.5.0=py_0 + - six=1.15.0=pyh9f0ad1d_0 + - sortedcontainers=2.3.0=pyhd8ed1ab_0 + - testpath=0.4.4=py_0 + - toml=0.10.2=pyhd8ed1ab_0 + - tqdm=4.54.1=pyhd8ed1ab_0 + - traitlets=5.0.5=py_0 + - twine=3.2.0=py37hc8dfbb8_1 + - typing_extensions=3.7.4.3=py_0 + - urllib3=1.25.11=py_0 + - wcwidth=0.2.5=pyh9f0ad1d_2 + - webencodings=0.5.1=py_1 + - wheel=0.36.1=pyhd3deb0d_0 + - zipp=3.4.0=py_0 unix: - - libblas=3.9.0=3_openblas - - libcblas=3.9.0=3_openblas - - liblapack=3.9.0=3_openblas + - libblas=3.9.0=3_openblas + - libcblas=3.9.0=3_openblas + - liblapack=3.9.0=3_openblas linux-64: - - _libgcc_mutex=0.1=conda_forge - - _openmp_mutex=4.5=1_gnu - - ansi2html=1.6.0=py37h89c1867_0 - - argon2-cffi=20.1.0=py37h8f50634_2 - - brotlipy=0.7.0=py37hb5d75c8_1001 - - ca-certificates=2020.12.5=ha878542_0 - - certifi=2020.12.5=py37h89c1867_0 - - cffi=1.14.4=py37hc58025e_1 - - chardet=3.0.4=py37he5f6b98_1008 - - cmarkgfm=0.4.2=py37h8f50634_3 - - coverage=5.3=py37h8f50634_1 - - cryptography=3.2.1=py37hc72a4ac_0 - - dataclasses=0.7=py37_0 - - dbus=1.13.18=hb2f20db_0 - - docutils=0.16=py37he5f6b98_2 - - entrypoints=0.3=py37hc8dfbb8_1002 - - expat=2.2.10=he6710b0_2 - - gettext=0.19.8.1=h0b5b191_1005 - - glib=2.66.3=h9c3ff4c_1 - - icu=68.1=h58526e2_0 - - importlib_resources=3.3.0=py37h89c1867_0 - - ipykernel=5.3.4=py37hc6149b9_1 - - ipython=7.19.0=py37h888b3d9_0 - - jedi=0.17.2=py37hc8dfbb8_1 - - jeepney=0.6.0=pyhd8ed1ab_0 - - jsonschema=3.2.0=py37hc8dfbb8_1 - - jupyter_core=4.7.0=py37h89c1867_0 - - keyring=21.5.0=py37h89c1867_0 - - ld_impl_linux-64=2.35.1=hed1e6ac_0 - - libffi=3.3=h58526e2_2 - - libgcc-ng=9.3.0=h5dbcf3e_17 - - libgfortran-ng=7.5.0=hae1eefd_17 - - libgfortran4=7.5.0=hae1eefd_17 - - libglib=2.66.3=h1f3bc88_1 - - libgomp=9.3.0=h5dbcf3e_17 - - libiconv=1.16=h516909a_0 - - libopenblas=0.3.12=pthreads_hb3c22a3_1 - - libsodium=1.0.18=h516909a_1 - - libstdcxx-ng=9.3.0=h2ae2ef3_17 - - libuv=1.40.0=hd18ef5c_0 - - markupsafe=1.1.1=py37hb5d75c8_2 - - mistune=0.8.4=py37h8f50634_1002 - - nbconvert=6.0.7=py37h89c1867_3 - - ncurses=6.2=h58526e2_4 - - nodejs=14.15.1=h25f6087_0 - - notebook=6.1.5=py37h89c1867_0 - - numpy=1.19.4=py37h7e9df27_1 - - openssl=1.1.1h=h516909a_0 - - pandas=1.1.5=py37hdc94413_0 - - pandoc=2.11.2=h36c2ea0_0 - - pandocfilters=1.4.3=py37h06a4308_1 - - pcre=8.44=he1b5a44_0 - - pexpect=4.8.0=py37hc8dfbb8_1 - - pickleshare=0.7.5=py37hc8dfbb8_1002 - - pluggy=0.13.1=py37he5f6b98_3 - - ptyprocess=0.6.0=py37_1000 - - pyinotify=0.9.6=py37hc8dfbb8_1002 - - pyrsistent=0.17.3=py37h8f50634_1 - - pysocks=1.7.1=py37he5f6b98_2 - - pytest-asyncio=0.14.0=py37h89c1867_0 - - pytest-forked=1.3.0=py_0 - - pytest=6.1.2=py37h89c1867_0 - - python=3.7.9=h7579374_0 - - pyzmq=20.0.0=py37h5a562af_1 - - readline=8.0=he28a2e2_2 - - regex=2020.11.13=py37h4abf009_0 - - secretstorage=3.3.0=py37h89c1867_0 - - setuptools=51.0.0=py37h06a4308_2 - - sqlite=3.34.0=h74cdb3f_0 - - terminado=0.9.1=py37hc8dfbb8_1 - - tk=8.6.10=hed695b0_1 - - tornado=6.1=py37h4abf009_0 - - typed-ast=1.4.1=py37h4abf009_1 - - widgetsnbextension=3.5.1=py37hc8dfbb8_4 - - xz=5.2.5=h516909a_1 - - zeromq=4.3.3=h58526e2_3 - - zlib=1.2.11=h516909a_1010 + - _libgcc_mutex=0.1=conda_forge + - _openmp_mutex=4.5=1_gnu + - ansi2html=1.6.0=py37h89c1867_0 + - argon2-cffi=20.1.0=py37h8f50634_2 + - brotlipy=0.7.0=py37hb5d75c8_1001 + - ca-certificates=2020.12.5=ha878542_0 + - certifi=2020.12.5=py37h89c1867_0 + - cffi=1.14.4=py37hc58025e_1 + - chardet=3.0.4=py37he5f6b98_1008 + - cmarkgfm=0.4.2=py37h8f50634_3 + - coverage=5.3=py37h8f50634_1 + - cryptography=3.2.1=py37hc72a4ac_0 + - dataclasses=0.7=py37_0 + - dbus=1.13.18=hb2f20db_0 + - docutils=0.16=py37he5f6b98_2 + - entrypoints=0.3=py37hc8dfbb8_1002 + - expat=2.2.10=he6710b0_2 + - gettext=0.19.8.1=h0b5b191_1005 + - glib=2.66.3=h9c3ff4c_1 + - icu=68.1=h58526e2_0 + - importlib_resources=3.3.0=py37h89c1867_0 + - ipykernel=5.3.4=py37hc6149b9_1 + - ipython=7.19.0=py37h888b3d9_0 + - jedi=0.17.2=py37hc8dfbb8_1 + - jeepney=0.6.0=pyhd8ed1ab_0 + - jsonschema=3.2.0=py37hc8dfbb8_1 + - jupyter_core=4.7.0=py37h89c1867_0 + - keyring=21.5.0=py37h89c1867_0 + - ld_impl_linux-64=2.35.1=hed1e6ac_0 + - libffi=3.3=h58526e2_2 + - libgcc-ng=9.3.0=h5dbcf3e_17 + - libgfortran-ng=7.5.0=hae1eefd_17 + - libgfortran4=7.5.0=hae1eefd_17 + - libglib=2.66.3=h1f3bc88_1 + - libgomp=9.3.0=h5dbcf3e_17 + - libiconv=1.16=h516909a_0 + - libopenblas=0.3.12=pthreads_hb3c22a3_1 + - libsodium=1.0.18=h516909a_1 + - libstdcxx-ng=9.3.0=h2ae2ef3_17 + - libuv=1.40.0=hd18ef5c_0 + - markupsafe=1.1.1=py37hb5d75c8_2 + - mistune=0.8.4=py37h8f50634_1002 + - nbconvert=6.0.7=py37h89c1867_3 + - ncurses=6.2=h58526e2_4 + - nodejs=14.15.1=h25f6087_0 + - notebook=6.1.5=py37h89c1867_0 + - numpy=1.19.4=py37h7e9df27_1 + - openssl=1.1.1h=h516909a_0 + - pandas=1.1.5=py37hdc94413_0 + - pandoc=2.11.2=h36c2ea0_0 + - pandocfilters=1.4.3=py37h06a4308_1 + - pcre=8.44=he1b5a44_0 + - pexpect=4.8.0=py37hc8dfbb8_1 + - pickleshare=0.7.5=py37hc8dfbb8_1002 + - pluggy=0.13.1=py37he5f6b98_3 + - ptyprocess=0.6.0=py37_1000 + - pyinotify=0.9.6=py37hc8dfbb8_1002 + - pyrsistent=0.17.3=py37h8f50634_1 + - pysocks=1.7.1=py37he5f6b98_2 + - pytest-asyncio=0.14.0=py37h89c1867_0 + - pytest-forked=1.3.0=py_0 + - pytest=6.1.2=py37h89c1867_0 + - python=3.7.9=h7579374_0 + - pyzmq=20.0.0=py37h5a562af_1 + - readline=8.0=he28a2e2_2 + - regex=2020.11.13=py37h4abf009_0 + - secretstorage=3.3.0=py37h89c1867_0 + - setuptools=51.0.0=py37h06a4308_2 + - sqlite=3.34.0=h74cdb3f_0 + - terminado=0.9.1=py37hc8dfbb8_1 + - tk=8.6.10=hed695b0_1 + - tornado=6.1=py37h4abf009_0 + - typed-ast=1.4.1=py37h4abf009_1 + - widgetsnbextension=3.5.1=py37hc8dfbb8_4 + - xz=5.2.5=h516909a_1 + - zeromq=4.3.3=h58526e2_3 + - zlib=1.2.11=h516909a_1010 osx-64: - - ansi2html=1.6.0=py37hf985489_0 - - appnope=0.1.2=py37hf985489_0 - - argon2-cffi=20.1.0=py37h4b544eb_2 - - brotlipy=0.7.0=py37h395d20d_1001 - - ca-certificates=2020.12.5=h033912b_0 - - certifi=2020.12.5=py37hf985489_0 - - cffi=1.14.4=py37hc5b2277_1 - - chardet=3.0.4=py37h2987424_1008 - - cmarkgfm=0.4.2=py37h60d8a13_3 - - coverage=5.3=py37h60d8a13_1 - - cryptography=3.2.1=py37h3b7a55b_0 - - dataclasses=0.7=pyhb2cacf7_7 - - docutils=0.16=py37h2987424_2 - - entrypoints=0.3=pyhd8ed1ab_1003 - - icu=68.1=h74dc148_0 - - importlib_resources=3.3.0=py37hf985489_0 - - ipykernel=5.3.4=py37he01cfaa_1 - - ipython=7.19.0=py37he01cfaa_0 - - jedi=0.17.2=py37hf985489_1 - - jsonschema=3.2.0=py_2 - - jupyter_core=4.7.0=py37hf985489_0 - - keyring=21.5.0=py37hf985489_0 - - libcxx=11.0.0=h4c3b8ed_1 - - libffi=3.3=h046ec9c_2 - - libgfortran5=9.3.0=h7cc5361_13 - - libgfortran=5.0.0=h7cc5361_13 - - libopenblas=0.3.12=openmp_h54245bb_1 - - libsodium=1.0.18=hbcb3906_1 - - libuv=1.40.0=h22f3db7_0 - - llvm-openmp=11.0.0=h73239a0_1 - - macfsevents=0.8.1=py37h60d8a13_1001 - - markupsafe=1.1.1=py37h395d20d_2 - - mistune=0.8.4=py37h4b544eb_1002 - - nbconvert=6.0.7=py37hf985489_3 - - ncurses=6.2=h2e338ed_4 - - nodejs=14.15.1=heaeed3d_0 - - notebook=6.1.5=py37hf985489_0 - - numpy=1.19.4=py37h9ebeaaa_1 - - openssl=1.1.1h=haf1e3a3_0 - - pandas=1.1.5=py37h010c265_0 - - pandoc=2.11.2=hc929b4f_0 - - pandocfilters=1.4.2=py_1 - - pexpect=4.8.0=pyh9f0ad1d_2 - - pickleshare=0.7.5=py_1003 - - pluggy=0.13.1=py37h2987424_3 - - ptyprocess=0.6.0=py_1001 - - pyrsistent=0.17.3=py37h4b544eb_1 - - pysocks=1.7.1=py37h2987424_2 - - pytest-asyncio=0.14.0=py37hf985489_0 - - pytest-forked=1.2.0=pyh9f0ad1d_0 - - pytest=6.1.2=py37hf985489_0 - - python=3.7.8=h4f09611_3_cpython - - pyzmq=20.0.0=py37h47fd9b3_1 - - readline=8.0=h0678c8f_2 - - regex=2020.11.13=py37h4b544eb_0 - - setuptools=49.6.0=py37h2987424_2 - - sqlite=3.34.0=h17101e1_0 - - terminado=0.9.1=py37hf985489_1 - - tk=8.6.10=hb0a8c7a_1 - - tornado=6.1=py37h4b544eb_0 - - typed-ast=1.4.1=py37h4b544eb_1 - - widgetsnbextension=3.5.1=py37hf985489_4 - - xz=5.2.5=haf1e3a3_1 - - zeromq=4.3.3=h74dc148_3 - - zlib=1.2.11=h7795811_1010 + - ansi2html=1.6.0=py37hf985489_0 + - appnope=0.1.2=py37hf985489_0 + - argon2-cffi=20.1.0=py37h4b544eb_2 + - brotlipy=0.7.0=py37h395d20d_1001 + - ca-certificates=2020.12.5=h033912b_0 + - certifi=2020.12.5=py37hf985489_0 + - cffi=1.14.4=py37hc5b2277_1 + - chardet=3.0.4=py37h2987424_1008 + - cmarkgfm=0.4.2=py37h60d8a13_3 + - coverage=5.3=py37h60d8a13_1 + - cryptography=3.2.1=py37h3b7a55b_0 + - dataclasses=0.7=pyhb2cacf7_7 + - docutils=0.16=py37h2987424_2 + - entrypoints=0.3=pyhd8ed1ab_1003 + - icu=68.1=h74dc148_0 + - importlib_resources=3.3.0=py37hf985489_0 + - ipykernel=5.3.4=py37he01cfaa_1 + - ipython=7.19.0=py37he01cfaa_0 + - jedi=0.17.2=py37hf985489_1 + - jsonschema=3.2.0=py_2 + - jupyter_core=4.7.0=py37hf985489_0 + - keyring=21.5.0=py37hf985489_0 + - libcxx=11.0.0=h4c3b8ed_1 + - libffi=3.3=h046ec9c_2 + - libgfortran5=9.3.0=h7cc5361_13 + - libgfortran=5.0.0=h7cc5361_13 + - libopenblas=0.3.12=openmp_h54245bb_1 + - libsodium=1.0.18=hbcb3906_1 + - libuv=1.40.0=h22f3db7_0 + - llvm-openmp=11.0.0=h73239a0_1 + - macfsevents=0.8.1=py37h60d8a13_1001 + - markupsafe=1.1.1=py37h395d20d_2 + - mistune=0.8.4=py37h4b544eb_1002 + - nbconvert=6.0.7=py37hf985489_3 + - ncurses=6.2=h2e338ed_4 + - nodejs=14.15.1=heaeed3d_0 + - notebook=6.1.5=py37hf985489_0 + - numpy=1.19.4=py37h9ebeaaa_1 + - openssl=1.1.1h=haf1e3a3_0 + - pandas=1.1.5=py37h010c265_0 + - pandoc=2.11.2=hc929b4f_0 + - pandocfilters=1.4.2=py_1 + - pexpect=4.8.0=pyh9f0ad1d_2 + - pickleshare=0.7.5=py_1003 + - pluggy=0.13.1=py37h2987424_3 + - ptyprocess=0.6.0=py_1001 + - pyrsistent=0.17.3=py37h4b544eb_1 + - pysocks=1.7.1=py37h2987424_2 + - pytest-asyncio=0.14.0=py37hf985489_0 + - pytest-forked=1.2.0=pyh9f0ad1d_0 + - pytest=6.1.2=py37hf985489_0 + - python=3.7.8=h4f09611_3_cpython + - pyzmq=20.0.0=py37h47fd9b3_1 + - readline=8.0=h0678c8f_2 + - regex=2020.11.13=py37h4b544eb_0 + - setuptools=49.6.0=py37h2987424_2 + - sqlite=3.34.0=h17101e1_0 + - terminado=0.9.1=py37hf985489_1 + - tk=8.6.10=hb0a8c7a_1 + - tornado=6.1=py37h4b544eb_0 + - typed-ast=1.4.1=py37h4b544eb_1 + - widgetsnbextension=3.5.1=py37hf985489_4 + - xz=5.2.5=haf1e3a3_1 + - zeromq=4.3.3=h74dc148_3 + - zlib=1.2.11=h7795811_1010 win-64: - - ansi2html=1.6.0=py37h03978a9_0 - - argon2-cffi=20.1.0=py37hcc03f2d_2 - - atomicwrites=1.4.0=pyh9f0ad1d_0 - - brotlipy=0.7.0=py37h0013d47_1001 - - ca-certificates=2020.12.5=h5b45459_0 - - certifi=2020.12.5=py37h03978a9_0 - - cffi=1.14.4=py37hd8e9650_1 - - chardet=3.0.4=py37hf50a25e_1008 - - cmarkgfm=0.4.2=py37h4ab8f01_3 - - coverage=5.3=py37h4ab8f01_1 - - cryptography=3.2.1=py37hd8e9650_0 - - dataclasses=0.7=pyhb2cacf7_7 - - docutils=0.16=py37hf50a25e_2 - - entrypoints=0.3=pyhd8ed1ab_1003 - - importlib_resources=3.3.0=py37h03978a9_0 - - intel-openmp=2020.3=h57928b3_311 - - ipykernel=5.3.4=py37h7b7c402_1 - - ipython=7.19.0=py37heaed05f_0 - - jedi=0.17.2=py37h03978a9_1 - - jsonschema=3.2.0=py_2 - - jupyter_core=4.7.0=py37h03978a9_0 - - keyring=21.5.0=py37h03978a9_0 - - libblas=3.8.0=21_mkl - - libcblas=3.8.0=21_mkl - - liblapack=3.8.0=21_mkl - - libsodium=1.0.18=h8d14728_1 - - m2w64-gcc-libgfortran=5.3.0=6 - - m2w64-gcc-libs-core=5.3.0=7 - - m2w64-gcc-libs=5.3.0=7 - - m2w64-gmp=6.1.0=2 - - m2w64-libwinpthread-git=5.0.0.4634.697f757=2 - - markupsafe=1.1.1=py37h0013d47_2 - - mistune=0.8.4=py37hcc03f2d_1002 - - mkl=2020.4=hb70f87d_311 - - msys2-conda-epoch=20160418=1 - - nbconvert=6.0.7=py37h03978a9_3 - - nodejs=14.15.1=h57928b3_0 - - notebook=6.1.5=py37h03978a9_0 - - numpy=1.19.4=py37hd20adf4_1 - - openssl=1.1.1h=he774522_0 - - pandas=1.1.5=py37h08fd248_0 - - pandoc=2.11.2=h8ffe710_0 - - pandocfilters=1.4.2=py_1 - - pickleshare=0.7.5=py_1003 - - pluggy=0.13.1=py37hf50a25e_3 - - pyrsistent=0.17.3=py37hcc03f2d_1 - - pysocks=1.7.1=py37hf50a25e_2 - - pytest-asyncio=0.14.0=py37h03978a9_0 - - pytest-forked=1.2.0=pyh9f0ad1d_0 - - pytest=6.1.2=py37h03978a9_0 - - python=3.7.8=h7840368_3_cpython - - pywin32-ctypes=0.2.0=py37hc8dfbb8_1002 - - pywin32=228=py37h4ab8f01_0 - - pywinpty=0.5.7=py37hc8dfbb8_1 - - pyzmq=20.0.0=py37h0d95fc2_1 - - regex=2020.11.13=py37hcc03f2d_0 - - setuptools=49.6.0=py37hf50a25e_2 - - sqlite=3.34.0=h8ffe710_0 - - terminado=0.9.1=py37h03978a9_1 - - tornado=6.1=py37hcc03f2d_0 - - typed-ast=1.4.1=py37hcc03f2d_1 - - vc=14.1=h869be7e_1 - - vs2015_runtime=14.16.27012=h30e32a0_2 - - widgetsnbextension=3.5.1=py37h03978a9_4 - - win_inet_pton=1.1.0=py37hc8dfbb8_1 - - wincertstore=0.2=py37hc8dfbb8_1005 - - winpty=0.4.3=4 - - zeromq=4.3.3=h0e60522_3 + - ansi2html=1.6.0=py37h03978a9_0 + - argon2-cffi=20.1.0=py37hcc03f2d_2 + - atomicwrites=1.4.0=pyh9f0ad1d_0 + - brotlipy=0.7.0=py37h0013d47_1001 + - ca-certificates=2020.12.5=h5b45459_0 + - certifi=2020.12.5=py37h03978a9_0 + - cffi=1.14.4=py37hd8e9650_1 + - chardet=3.0.4=py37hf50a25e_1008 + - cmarkgfm=0.4.2=py37h4ab8f01_3 + - coverage=5.3=py37h4ab8f01_1 + - cryptography=3.2.1=py37hd8e9650_0 + - dataclasses=0.7=pyhb2cacf7_7 + - docutils=0.16=py37hf50a25e_2 + - entrypoints=0.3=pyhd8ed1ab_1003 + - importlib_resources=3.3.0=py37h03978a9_0 + - intel-openmp=2020.3=h57928b3_311 + - ipykernel=5.3.4=py37h7b7c402_1 + - ipython=7.19.0=py37heaed05f_0 + - jedi=0.17.2=py37h03978a9_1 + - jsonschema=3.2.0=py_2 + - jupyter_core=4.7.0=py37h03978a9_0 + - keyring=21.5.0=py37h03978a9_0 + - libblas=3.8.0=21_mkl + - libcblas=3.8.0=21_mkl + - liblapack=3.8.0=21_mkl + - libsodium=1.0.18=h8d14728_1 + - m2w64-gcc-libgfortran=5.3.0=6 + - m2w64-gcc-libs-core=5.3.0=7 + - m2w64-gcc-libs=5.3.0=7 + - m2w64-gmp=6.1.0=2 + - m2w64-libwinpthread-git=5.0.0.4634.697f757=2 + - markupsafe=1.1.1=py37h0013d47_2 + - mistune=0.8.4=py37hcc03f2d_1002 + - mkl=2020.4=hb70f87d_311 + - msys2-conda-epoch=20160418=1 + - nbconvert=6.0.7=py37h03978a9_3 + - nodejs=14.15.1=h57928b3_0 + - notebook=6.1.5=py37h03978a9_0 + - numpy=1.19.4=py37hd20adf4_1 + - openssl=1.1.1h=he774522_0 + - pandas=1.1.5=py37h08fd248_0 + - pandoc=2.11.2=h8ffe710_0 + - pandocfilters=1.4.2=py_1 + - pickleshare=0.7.5=py_1003 + - pluggy=0.13.1=py37hf50a25e_3 + - pyrsistent=0.17.3=py37hcc03f2d_1 + - pysocks=1.7.1=py37hf50a25e_2 + - pytest-asyncio=0.14.0=py37h03978a9_0 + - pytest-forked=1.2.0=pyh9f0ad1d_0 + - pytest=6.1.2=py37h03978a9_0 + - python=3.7.8=h7840368_3_cpython + - pywin32-ctypes=0.2.0=py37hc8dfbb8_1002 + - pywin32=228=py37h4ab8f01_0 + - pywinpty=0.5.7=py37hc8dfbb8_1 + - pyzmq=20.0.0=py37h0d95fc2_1 + - regex=2020.11.13=py37hcc03f2d_0 + - setuptools=49.6.0=py37hf50a25e_2 + - sqlite=3.34.0=h8ffe710_0 + - terminado=0.9.1=py37h03978a9_1 + - tornado=6.1=py37hcc03f2d_0 + - typed-ast=1.4.1=py37hcc03f2d_1 + - vc=14.1=h869be7e_1 + - vs2015_runtime=14.16.27012=h30e32a0_2 + - widgetsnbextension=3.5.1=py37h03978a9_4 + - win_inet_pton=1.1.0=py37hc8dfbb8_1 + - wincertstore=0.2=py37hc8dfbb8_1005 + - winpty=0.4.3=4 + - zeromq=4.3.3=h0e60522_3 atest: locked: true env_spec_hash: a3cbc653618884c4eb2c0cda4abc326eac9894ad platforms: - - linux-64 - - osx-64 - - win-64 + - linux-64 + - osx-64 + - win-64 packages: all: - - idna=2.10=pyh9f0ad1d_0 - - pip=20.3.1=pyhd8ed1ab_0 - - pycparser=2.20=pyh9f0ad1d_2 - - pyopenssl=20.0.0=pyhd8ed1ab_0 - - python_abi=3.7=1_cp37m - - robotframework-lint=1.1=pyh9f0ad1d_0 - - robotframework-pabot=1.10.0=py_0 - - robotframework-pythonlibcore=2.1.0=pyhd8ed1ab_1 - - robotframework-seleniumlibrary=4.5.0=pyh9f0ad1d_0 - - robotframework=3.2.2=pyh9f0ad1d_0 - - six=1.15.0=pyh9f0ad1d_0 - - urllib3=1.26.2=pyhd8ed1ab_0 - - wheel=0.36.1=pyhd3deb0d_0 + - idna=2.10=pyh9f0ad1d_0 + - pip=20.3.1=pyhd8ed1ab_0 + - pycparser=2.20=pyh9f0ad1d_2 + - pyopenssl=20.0.0=pyhd8ed1ab_0 + - python_abi=3.7=1_cp37m + - robotframework-lint=1.1=pyh9f0ad1d_0 + - robotframework-pabot=1.10.0=py_0 + - robotframework-pythonlibcore=2.1.0=pyhd8ed1ab_1 + - robotframework-seleniumlibrary=4.5.0=pyh9f0ad1d_0 + - robotframework=3.2.2=pyh9f0ad1d_0 + - six=1.15.0=pyh9f0ad1d_0 + - urllib3=1.26.2=pyhd8ed1ab_0 + - wheel=0.36.1=pyhd3deb0d_0 linux-64: - - _libgcc_mutex=0.1=conda_forge - - _openmp_mutex=4.5=1_gnu - - brotlipy=0.7.0=py37hb5d75c8_1001 - - ca-certificates=2020.11.8=ha878542_0 - - certifi=2020.11.8=py37h89c1867_0 - - cffi=1.14.4=py37hc58025e_1 - - cryptography=3.2.1=py37hc72a4ac_0 - - firefox=83.0=h58526e2_0 - - geckodriver=0.28.0=h58526e2_0 - - ld_impl_linux-64=2.35.1=hed1e6ac_0 - - libffi=3.3=h58526e2_1 - - libgcc-ng=9.3.0=h5dbcf3e_17 - - libgomp=9.3.0=h5dbcf3e_17 - - libstdcxx-ng=9.3.0=h2ae2ef3_17 - - ncurses=6.2=h58526e2_4 - - openssl=1.1.1h=h516909a_0 - - pysocks=1.7.1=py37he5f6b98_2 - - python=3.7.8=hffdb5ce_3_cpython - - readline=8.0=he28a2e2_2 - - selenium=3.141.0=py37h8f50634_1002 - - setuptools=49.6.0=py37he5f6b98_2 - - sqlite=3.34.0=h74cdb3f_0 - - tk=8.6.10=hed695b0_1 - - xz=5.2.5=h516909a_1 - - zlib=1.2.11=h516909a_1010 + - _libgcc_mutex=0.1=conda_forge + - _openmp_mutex=4.5=1_gnu + - brotlipy=0.7.0=py37hb5d75c8_1001 + - ca-certificates=2020.11.8=ha878542_0 + - certifi=2020.11.8=py37h89c1867_0 + - cffi=1.14.4=py37hc58025e_1 + - cryptography=3.2.1=py37hc72a4ac_0 + - firefox=83.0=h58526e2_0 + - geckodriver=0.28.0=h58526e2_0 + - ld_impl_linux-64=2.35.1=hed1e6ac_0 + - libffi=3.3=h58526e2_1 + - libgcc-ng=9.3.0=h5dbcf3e_17 + - libgomp=9.3.0=h5dbcf3e_17 + - libstdcxx-ng=9.3.0=h2ae2ef3_17 + - ncurses=6.2=h58526e2_4 + - openssl=1.1.1h=h516909a_0 + - pysocks=1.7.1=py37he5f6b98_2 + - python=3.7.8=hffdb5ce_3_cpython + - readline=8.0=he28a2e2_2 + - selenium=3.141.0=py37h8f50634_1002 + - setuptools=49.6.0=py37he5f6b98_2 + - sqlite=3.34.0=h74cdb3f_0 + - tk=8.6.10=hed695b0_1 + - xz=5.2.5=h516909a_1 + - zlib=1.2.11=h516909a_1010 osx-64: - - brotlipy=0.7.0=py37h395d20d_1001 - - ca-certificates=2020.11.8=h033912b_0 - - certifi=2020.11.8=py37hf985489_0 - - cffi=1.14.4=py37hc5b2277_1 - - cryptography=3.2.1=py37h3b7a55b_0 - - firefox=83.0=h74dc148_0 - - geckodriver=0.28.0=h2e338ed_0 - - libcxx=11.0.0=h4c3b8ed_1 - - libffi=3.3=h74dc148_1 - - ncurses=6.2=h2e338ed_4 - - openssl=1.1.1h=haf1e3a3_0 - - pysocks=1.7.1=py37h2987424_2 - - python=3.7.8=h4f09611_3_cpython - - readline=8.0=h0678c8f_2 - - selenium=3.141.0=py37h60d8a13_1002 - - setuptools=49.6.0=py37h2987424_2 - - sqlite=3.34.0=h17101e1_0 - - tk=8.6.10=hb0a8c7a_1 - - xz=5.2.5=haf1e3a3_1 - - zlib=1.2.11=h7795811_1010 + - brotlipy=0.7.0=py37h395d20d_1001 + - ca-certificates=2020.11.8=h033912b_0 + - certifi=2020.11.8=py37hf985489_0 + - cffi=1.14.4=py37hc5b2277_1 + - cryptography=3.2.1=py37h3b7a55b_0 + - firefox=83.0=h74dc148_0 + - geckodriver=0.28.0=h2e338ed_0 + - libcxx=11.0.0=h4c3b8ed_1 + - libffi=3.3=h74dc148_1 + - ncurses=6.2=h2e338ed_4 + - openssl=1.1.1h=haf1e3a3_0 + - pysocks=1.7.1=py37h2987424_2 + - python=3.7.8=h4f09611_3_cpython + - readline=8.0=h0678c8f_2 + - selenium=3.141.0=py37h60d8a13_1002 + - setuptools=49.6.0=py37h2987424_2 + - sqlite=3.34.0=h17101e1_0 + - tk=8.6.10=hb0a8c7a_1 + - xz=5.2.5=haf1e3a3_1 + - zlib=1.2.11=h7795811_1010 win-64: - - brotlipy=0.7.0=py37h0013d47_1001 - - ca-certificates=2020.11.8=h5b45459_0 - - certifi=2020.11.8=py37h03978a9_0 - - cffi=1.14.4=py37hd8e9650_1 - - cryptography=3.2.1=py37hd8e9650_0 - - firefox=83.0=h0e60522_0 - - geckodriver=0.28.0=h39d44d4_0 - - openssl=1.1.1h=he774522_0 - - pysocks=1.7.1=py37hf50a25e_2 - - python=3.7.8=h7840368_3_cpython - - selenium=3.141.0=py37h4ab8f01_1002 - - setuptools=49.6.0=py37hf50a25e_2 - - sqlite=3.34.0=h8ffe710_0 - - vc=14.1=h869be7e_1 - - vs2015_runtime=14.16.27012=h30e32a0_2 - - win_inet_pton=1.1.0=py37hc8dfbb8_1 - - wincertstore=0.2=py37hc8dfbb8_1005 + - brotlipy=0.7.0=py37h0013d47_1001 + - ca-certificates=2020.11.8=h5b45459_0 + - certifi=2020.11.8=py37h03978a9_0 + - cffi=1.14.4=py37hd8e9650_1 + - cryptography=3.2.1=py37hd8e9650_0 + - firefox=83.0=h0e60522_0 + - geckodriver=0.28.0=h39d44d4_0 + - openssl=1.1.1h=he774522_0 + - pysocks=1.7.1=py37hf50a25e_2 + - python=3.7.8=h7840368_3_cpython + - selenium=3.141.0=py37h4ab8f01_1002 + - setuptools=49.6.0=py37hf50a25e_2 + - sqlite=3.34.0=h8ffe710_0 + - vc=14.1=h869be7e_1 + - vs2015_runtime=14.16.27012=h30e32a0_2 + - win_inet_pton=1.1.0=py37hc8dfbb8_1 + - wincertstore=0.2=py37hc8dfbb8_1005 diff --git a/dodo.py b/dodo.py index a18cb1f6..31604880 100644 --- a/dodo.py +++ b/dodo.py @@ -17,6 +17,7 @@ from doit.action import CmdAction from doit.tools import PythonInteractiveAction, config_changed + from scripts import project as P from scripts import reporter from scripts import utils as U diff --git a/py_src/ipyelk/contrib/doit/ipydoit.py b/py_src/ipyelk/contrib/doit/ipydoit.py index eb7b9cd4..6174e895 100644 --- a/py_src/ipyelk/contrib/doit/ipydoit.py +++ b/py_src/ipyelk/contrib/doit/ipydoit.py @@ -190,7 +190,9 @@ def _update_diagram(self, change=None): else: # file dependency outside of doit graph.add_node(dep_id, properties={"cssClasses": "dodo_file"}) - graph.add_edge(dep_id, task_id, targetPort=dep_id) + graph.add_edge( + dep_id, task_id, sourcePort="exists", targetPort=dep_id + ) for f in relative(t.targets, self.root): dep_id = f diff --git a/py_src/ipyelk/nx/nx.py b/py_src/ipyelk/nx/nx.py index 957a9ed5..83aa140a 100644 --- a/py_src/ipyelk/nx/nx.py +++ b/py_src/ipyelk/nx/nx.py @@ -166,3 +166,22 @@ def map_visible(g: nx.Graph, tree: nx.DiGraph, attr: str) -> Dict[Hashable, Hash if n not in mapping: mapping[n] = n return mapping + + +def stats(g:nx.Graph): + """Utility function get get some high level graph metrics + + :param g: Input networkx graph + :type g: nx.Graph + :return: dict of metrics + :rtype: Dict + """ + num_ports = 0 + for n, data in g.nodes(data=True): + ports = data.get("ports", []) + num_ports += len(ports) + return { + "nodes": len(g), + "edges": len(g.edges), + "ports": num_ports, + } \ No newline at end of file