Skip to content

Commit 2fb29ae

Browse files
authored
[cards] PythonCode component (Netflix#2196)
1 parent 0631b73 commit 2fb29ae

File tree

14 files changed

+213
-60
lines changed

14 files changed

+213
-60
lines changed

docs/cards.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ PATH_TO_CUSTOM_HTML = 'myhtml.html'
121121
class CustomCard(MetaflowCard):
122122
type = "custom_card"
123123

124-
def __init__(self, options={"no_header": True}, graph=None,components=[]):
124+
def __init__(self, options={"no_header": True}, graph=None, components=[], flow=None, **kwargs):
125125
super().__init__()
126126
self._no_header = True
127127
self._graph = graph
@@ -177,7 +177,7 @@ class CustomCard(MetaflowCard):
177177

178178
HTML = "<html><head></head><body>{data}<body></html>"
179179

180-
def __init__(self, options={"no_header": True}, graph=None,components=[]):
180+
def __init__(self, options={"no_header": True}, graph=None, components=[], flow=None, **kwargs):
181181
super().__init__()
182182
self._no_header = True
183183
self._graph = graph
@@ -276,7 +276,7 @@ class YCard(MetaflowCard):
276276

277277
ALLOW_USER_COMPONENTS = True
278278

279-
def __init__(self, options={}, components=[], graph=None):
279+
def __init__(self, options={}, components=[], graph=None, flow=None, **kwargs):
280280
self._components = components
281281

282282
def render(self, task):

metaflow/cards.py

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
Markdown,
99
VegaChart,
1010
ProgressBar,
11+
PythonCode,
1112
)
1213
from metaflow.plugins.cards.card_modules.basic import (
1314
DefaultCard,

metaflow/plugins/cards/card_cli.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -691,10 +691,15 @@ def create(
691691
try:
692692
if options is not None:
693693
mf_card = filtered_card(
694-
options=options, components=component_arr, graph=graph_dict
694+
options=options,
695+
components=component_arr,
696+
graph=graph_dict,
697+
flow=ctx.obj.flow,
695698
)
696699
else:
697-
mf_card = filtered_card(components=component_arr, graph=graph_dict)
700+
mf_card = filtered_card(
701+
components=component_arr, graph=graph_dict, flow=ctx.obj.flow
702+
)
698703
except TypeError as e:
699704
if render_error_card:
700705
mf_card = None

metaflow/plugins/cards/card_modules/basic.py

+56-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import base64
22
import json
33
import os
4-
from .card import MetaflowCard, MetaflowCardComponent
4+
from .card import MetaflowCard, MetaflowCardComponent, with_default_component_id
55
from .convert_to_native_type import TaskToDict
66
import uuid
7+
import inspect
78

89
ABS_DIR_PATH = os.path.dirname(os.path.abspath(__file__))
910
RENDER_TEMPLATE_PATH = os.path.join(ABS_DIR_PATH, "base.html")
@@ -236,9 +237,28 @@ def __init__(self, data=None):
236237
super().__init__(title=None, subtitle=None)
237238
self._data = data
238239

240+
@with_default_component_id
239241
def render(self):
240242
datadict = super().render()
241243
datadict["data"] = self._data
244+
if self.component_id is not None:
245+
datadict["id"] = self.component_id
246+
return datadict
247+
248+
249+
class PythonCodeComponent(DefaultComponent):
250+
251+
type = "pythonCode"
252+
253+
def __init__(self, data=None):
254+
super().__init__(title=None, subtitle=None)
255+
self._data = data
256+
257+
def render(self):
258+
datadict = super().render()
259+
datadict["data"] = self._data
260+
if self.component_id is not None:
261+
datadict["id"] = self.component_id
242262
return datadict
243263

244264

@@ -343,6 +363,7 @@ def __init__(
343363
graph=None,
344364
components=[],
345365
runtime=False,
366+
flow=None,
346367
):
347368
self._task = task
348369
self._only_repr = only_repr
@@ -352,6 +373,7 @@ def __init__(
352373
self.final_component = None
353374
self.page_component = None
354375
self.runtime = runtime
376+
self.flow = flow
355377

356378
def render(self):
357379
"""
@@ -475,6 +497,16 @@ def render(self):
475497
contents=[param_component],
476498
).render()
477499

500+
step_func = getattr(self.flow, self._task.parent.id)
501+
code_table = SectionComponent(
502+
title="Task Code",
503+
contents=[
504+
TableComponent(
505+
data=[[PythonCodeComponent(inspect.getsource(step_func)).render()]]
506+
)
507+
],
508+
).render()
509+
478510
# Don't include parameter ids + "name" in the task artifacts
479511
artifactlist = [
480512
task_data_dict["data"][k]
@@ -500,6 +532,7 @@ def render(self):
500532
page_contents.extend(
501533
[
502534
metadata_table,
535+
code_table,
503536
parameter_table,
504537
artifact_section,
505538
]
@@ -546,7 +579,7 @@ class ErrorCard(MetaflowCard):
546579

547580
RELOAD_POLICY = MetaflowCard.RELOAD_POLICY_ONCHANGE
548581

549-
def __init__(self, options={}, components=[], graph=None):
582+
def __init__(self, options={}, components=[], graph=None, **kwargs):
550583
self._only_repr = True
551584
self._graph = None if graph is None else transform_flow_graph(graph)
552585
self._components = components
@@ -602,9 +635,17 @@ class DefaultCardJSON(MetaflowCard):
602635

603636
type = "default_json"
604637

605-
def __init__(self, options=dict(only_repr=True), components=[], graph=None):
638+
def __init__(
639+
self,
640+
options=dict(only_repr=True),
641+
components=[],
642+
graph=None,
643+
flow=None,
644+
**kwargs
645+
):
606646
self._only_repr = True
607647
self._graph = None if graph is None else transform_flow_graph(graph)
648+
self._flow = flow
608649
if "only_repr" in options:
609650
self._only_repr = options["only_repr"]
610651
self._components = components
@@ -615,6 +656,7 @@ def render(self, task):
615656
only_repr=self._only_repr,
616657
graph=self._graph,
617658
components=self._components,
659+
flow=self._flow,
618660
).render()
619661
return json.dumps(final_component_dict)
620662

@@ -629,9 +671,17 @@ class DefaultCard(MetaflowCard):
629671

630672
type = "default"
631673

632-
def __init__(self, options=dict(only_repr=True), components=[], graph=None):
674+
def __init__(
675+
self,
676+
options=dict(only_repr=True),
677+
components=[],
678+
graph=None,
679+
flow=None,
680+
**kwargs
681+
):
633682
self._only_repr = True
634683
self._graph = None if graph is None else transform_flow_graph(graph)
684+
self._flow = flow
635685
if "only_repr" in options:
636686
self._only_repr = options["only_repr"]
637687
self._components = components
@@ -646,6 +696,7 @@ def render(self, task, runtime=False):
646696
graph=self._graph,
647697
components=self._components,
648698
runtime=runtime,
699+
flow=self._flow,
649700
).render()
650701
pt = self._get_mustache()
651702
data_dict = dict(
@@ -688,7 +739,7 @@ class BlankCard(MetaflowCard):
688739

689740
type = "blank"
690741

691-
def __init__(self, options=dict(title=""), components=[], graph=None):
742+
def __init__(self, options=dict(title=""), components=[], graph=None, **kwargs):
692743
self._graph = None if graph is None else transform_flow_graph(graph)
693744
self._title = ""
694745
if "title" in options:

metaflow/plugins/cards/card_modules/card.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import TYPE_CHECKING
2+
import uuid
23

34
if TYPE_CHECKING:
45
import metaflow
@@ -66,7 +67,7 @@ class MetaflowCard(object):
6667
# FIXME document runtime_data
6768
runtime_data = None
6869

69-
def __init__(self, options={}, components=[], graph=None):
70+
def __init__(self, options={}, components=[], graph=None, flow=None):
7071
pass
7172

7273
def _get_mustache(self):
@@ -140,3 +141,17 @@ def render(self):
140141
`render` returns a string or dictionary. This class can be called on the client side to dynamically add components to the `MetaflowCard`
141142
"""
142143
raise NotImplementedError()
144+
145+
146+
def create_component_id(component):
147+
uuid_bit = "".join(uuid.uuid4().hex.split("-"))[:6]
148+
return type(component).__name__.lower() + "_" + uuid_bit
149+
150+
151+
def with_default_component_id(func):
152+
def ret_func(self, *args, **kwargs):
153+
if self.component_id is None:
154+
self.component_id = create_component_id(self)
155+
return func(self, *args, **kwargs)
156+
157+
return ret_func

metaflow/plugins/cards/card_modules/components.py

+64-16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, List, Optional, Union
1+
from typing import Any, List, Optional, Union, Callable
22
from .basic import (
33
LogComponent,
44
ErrorComponent,
@@ -7,25 +7,13 @@
77
ImageComponent,
88
SectionComponent,
99
MarkdownComponent,
10+
PythonCodeComponent,
1011
)
11-
from .card import MetaflowCardComponent
12+
from .card import MetaflowCardComponent, with_default_component_id
1213
from .convert_to_native_type import TaskToDict, _full_classname
1314
from .renderer_tools import render_safely
1415
import uuid
15-
16-
17-
def create_component_id(component):
18-
uuid_bit = "".join(uuid.uuid4().hex.split("-"))[:6]
19-
return type(component).__name__.lower() + "_" + uuid_bit
20-
21-
22-
def with_default_component_id(func):
23-
def ret_func(self, *args, **kwargs):
24-
if self.component_id is None:
25-
self.component_id = create_component_id(self)
26-
return func(self, *args, **kwargs)
27-
28-
return ret_func
16+
import inspect
2917

3018

3119
def _warning_with_component(component, msg):
@@ -823,3 +811,63 @@ def render(self):
823811
if self._chart_inside_table and "autosize" not in self._spec:
824812
data["spec"]["autosize"] = "fit-x"
825813
return data
814+
815+
816+
class PythonCode(UserComponent):
817+
"""
818+
A component to display Python code with syntax highlighting.
819+
820+
Example:
821+
```python
822+
@card
823+
@step
824+
def my_step(self):
825+
# Using code_func
826+
def my_function():
827+
x = 1
828+
y = 2
829+
return x + y
830+
current.card.append(
831+
PythonCode(my_function)
832+
)
833+
834+
# Using code_string
835+
code = '''
836+
def another_function():
837+
return "Hello World"
838+
'''
839+
current.card.append(
840+
PythonCode(code_string=code)
841+
)
842+
```
843+
844+
Parameters
845+
----------
846+
code_func : Callable[..., Any], optional, default None
847+
The function whose source code should be displayed.
848+
code_string : str, optional, default None
849+
A string containing Python code to display.
850+
Either code_func or code_string must be provided.
851+
"""
852+
853+
def __init__(
854+
self,
855+
code_func: Optional[Callable[..., Any]] = None,
856+
code_string: Optional[str] = None,
857+
):
858+
if code_func is not None:
859+
self._code_string = inspect.getsource(code_func)
860+
else:
861+
self._code_string = code_string
862+
863+
@with_default_component_id
864+
@render_safely
865+
def render(self):
866+
if self._code_string is None:
867+
return ErrorComponent(
868+
"`PythonCode` component requires a `code_func` or `code_string` argument. ",
869+
"None provided for both",
870+
).render()
871+
_code_component = PythonCodeComponent(self._code_string)
872+
_code_component.component_id = self.component_id
873+
return _code_component.render()

metaflow/plugins/cards/card_modules/main.js

+27-25
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

metaflow/plugins/cards/card_modules/test_cards.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class TestEditableCard(MetaflowCard):
3838

3939
ALLOW_USER_COMPONENTS = True
4040

41-
def __init__(self, options={}, components=[], graph=None):
41+
def __init__(self, components=[], **kwargs):
4242
self._components = components
4343

4444
def render(self, task):
@@ -52,7 +52,7 @@ class TestEditableCard2(MetaflowCard):
5252

5353
ALLOW_USER_COMPONENTS = True
5454

55-
def __init__(self, options={}, components=[], graph=None):
55+
def __init__(self, components=[], **kwargs):
5656
self._components = components
5757

5858
def render(self, task):
@@ -64,7 +64,7 @@ class TestNonEditableCard(MetaflowCard):
6464

6565
seperator = "$&#!!@*"
6666

67-
def __init__(self, options={}, components=[], graph=None):
67+
def __init__(self, components=[], **kwargs):
6868
self._components = components
6969

7070
def render(self, task):
@@ -193,7 +193,7 @@ class TestRefreshComponentCard(MetaflowCard):
193193

194194
type = "test_component_refresh_card"
195195

196-
def __init__(self, options={}, components=[], graph=None):
196+
def __init__(self, components=[], **kwargs):
197197
self._components = components
198198

199199
def render(self, task) -> str:

metaflow/plugins/cards/component_serializer.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from .card_modules import MetaflowCardComponent
2+
from .card_modules.card import create_component_id
23
from .card_modules.basic import ErrorComponent, SectionComponent
34
from .card_modules.components import (
45
UserComponent,
5-
create_component_id,
66
StubComponent,
77
)
88
from .exception import ComponentOverwriteNotSupportedException

0 commit comments

Comments
 (0)