Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Python Dependency Deck #2264

Merged
merged 66 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
fccb28d
feat: refactor source code rendering in `SourceCodeDeck` class
jasonlai1218 Mar 11, 2024
1fec103
refactor: refactor deck rendering for Python dependencies
jasonlai1218 Mar 12, 2024
4ec42a7
refactor: refactor class properties and initialization
jasonlai1218 Mar 12, 2024
6e70e23
- feat: consolidate Python dependencies in `flytekit/__init__.py`
jasonlai1218 Mar 12, 2024
4dfeb34
refactor: refactor `flytekit/deck/deck.py` for `pandas` compatibility
jasonlai1218 Mar 12, 2024
27ec1f2
refactor: refactor return types across multiple files
jasonlai1218 Mar 12, 2024
5fd853d
feat: consolidate Python dependency management in FlyteContext
jasonlai1218 Mar 12, 2024
bc21848
refactor: refactor code to use DataFrame for package handling
jasonlai1218 Mar 13, 2024
9b30dc6
chore: refactor code for improved naming conventions
jasonlai1218 Mar 13, 2024
5ebc980
style: improve table alignment styling in CSS
jasonlai1218 Mar 13, 2024
501fcb6
refactor: refactor method and variable names across files
jasonlai1218 Mar 13, 2024
6ce4575
refactor: refactor imports in flytekit package
jasonlai1218 Mar 13, 2024
7556126
refactor: consolidate import statements in core/context_manager.py
jasonlai1218 Mar 13, 2024
534fa0f
style: improve code consistency and error checking
jasonlai1218 Mar 13, 2024
c4358f0
refactor: refactor Python dependency handling in classes
jasonlai1218 Mar 13, 2024
363f84f
chore: optimize imports in deck.py files
jasonlai1218 Mar 13, 2024
a775c7f
test: improve test coverage for PythonDependencyDeck class
jasonlai1218 Mar 13, 2024
a4773e0
feat: enhance user space deck management
jasonlai1218 Mar 13, 2024
05db9ab
refactor: refactor deck module and unit tests
jasonlai1218 Mar 13, 2024
d2bc23b
fix: update subprocess calls to use `sys.executable`
jasonlai1218 Mar 16, 2024
099a931
feat: refactor HTML generation logic and improve user experience
jasonlai1218 Mar 16, 2024
b02db7c
feat: enhance table content copying functionality
jasonlai1218 Mar 16, 2024
aaedf7a
refactor: improve table content copying functionality
jasonlai1218 Mar 16, 2024
938e0dd
refactor: improve package management and error handling
jasonlai1218 Mar 16, 2024
265cc60
chore: standardize whitespace in requirements_txt handling
jasonlai1218 Mar 16, 2024
7e132c0
style: standardize quotation marks for package_info keys
jasonlai1218 Mar 16, 2024
9eacfe7
Merge branch 'master' into add-py-deps-deck
jasonlai1218 Mar 16, 2024
20a3e9f
refactor: simplify requirements_txt generation
jasonlai1218 Mar 17, 2024
341a7da
docs: fix typos and improve code consistency across files
jasonlai1218 Mar 17, 2024
de90326
refactor: refactor dependency handling in PythonDependencyDeck class
jasonlai1218 Mar 18, 2024
b5f49b4
Merge branch 'master' into add-py-deps-deck
jasonlai1218 Mar 18, 2024
5634e78
refactor: update default name and test assertion in PythonDependencyDeck
jasonlai1218 Mar 18, 2024
9bd7995
Merge branch 'master' into add-py-deps-deck
jasonlai1218 Mar 19, 2024
79a09ac
Merge branch 'master' into add-py-deps-deck
jasonlai1218 Mar 20, 2024
e4c0bd0
refactor: update rendering of Pandas DataFrame using MarkdownRenderer
jasonlai1218 Mar 25, 2024
0091f25
Merge branch 'master' into add-py-deps-deck
jasonlai1218 Mar 25, 2024
5a58a8e
refactor: refactor PythonDependencyDeck and related classes
jasonlai1218 Mar 25, 2024
12ae643
feat: use `TableRenderer` for rendering DataFrames
jasonlai1218 Mar 26, 2024
16e681c
test: refactor codebase for improved performance
jasonlai1218 Mar 26, 2024
fc21591
Merge branch 'master' into add-py-deps-deck
jasonlai1218 Mar 26, 2024
ac21522
test: update test_deck.py for python dependency deck testing
jasonlai1218 Mar 26, 2024
96a852c
style: standardize import statements for pandas in project files
jasonlai1218 Mar 26, 2024
20b84fc
refactor: refactor import statements for `TableRenderer` usage
jasonlai1218 Mar 27, 2024
8572008
refactor: update PythonDependencyRenderer description
jasonlai1218 Mar 27, 2024
1c93057
refactor: refactor type hints and assertions across files
jasonlai1218 Mar 27, 2024
a32ce2d
Merge branch 'master' into add-py-deps-deck
jasonlai1218 Mar 27, 2024
8a40607
feat: refactor rendering classes in deck and plugins
jasonlai1218 Mar 29, 2024
f731757
docs: standardize markdown formatting across files
jasonlai1218 Mar 29, 2024
20e96c5
refactor: refactor markdown table rendering in PythonDependencyRenderer
jasonlai1218 Mar 29, 2024
352b79b
style: improve code consistency in PythonDependencyRenderer
jasonlai1218 Mar 29, 2024
0aad322
style: standardize formatting for better readability
jasonlai1218 Mar 29, 2024
7f40ad8
fix: update assertion to check for `Name` and `Version` consistency
jasonlai1218 Mar 29, 2024
1885662
test: extend test deadlines across various tests
jasonlai1218 Mar 29, 2024
3a262c4
Revert "test: extend test deadlines across various tests"
eapolinario Mar 29, 2024
1534070
Only run generate decks in python_function_task if decks are enabled
eapolinario Mar 29, 2024
03cb07c
Remove breakpoint
eapolinario Mar 29, 2024
6153065
Fix test_deck.py tests to account for the number of expected decks (n…
eapolinario Mar 29, 2024
3574aad
Increase deadline of eager tests
eapolinario Mar 29, 2024
edb7c18
Fix lint error
eapolinario Mar 30, 2024
57597ed
Merge branch 'master' into add-py-deps-deck
jasonlai1218 Mar 30, 2024
9a2470e
test: update test assertions in test_deck.py
jasonlai1218 Mar 30, 2024
fc81271
refactor: refactor Python code organization
jasonlai1218 Apr 2, 2024
0f5a455
Merge branch 'master' into add-py-deps-deck
jasonlai1218 Apr 2, 2024
ef47b14
docs: refactor project structure and improve user experience
jasonlai1218 Apr 3, 2024
8c9af6b
Merge branch 'master' into add-py-deps-deck
jasonlai1218 Apr 3, 2024
814a952
Separate out tests that require hypothesis
eapolinario Apr 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/pythonbuild.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,41 @@ jobs:
fail_ci_if_error: false
files: coverage.xml

test-hypothesis:
needs:
- detect-python-versions
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
python-version: ${{fromJson(needs.detect-python-versions.outputs.python-versions)}}
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Cache pip
uses: actions/cache@v3
with:
# This path is specific to Ubuntu
path: ~/.cache/pip
# Look to see if there is a cache hit for the corresponding requirements files
key: ${{ format('{0}-pip-{1}', runner.os, hashFiles('dev-requirements.in', 'requirements.in')) }}
- name: Install dependencies
run: make setup && pip freeze
- name: Test with coverage
env:
FLYTEKIT_HYPOTHESIS_PROFILE: ci
run: |
make unit_test_hypothesis
- name: Codecov
uses: codecov/[email protected]
with:
fail_ci_if_error: false
files: coverage.xml

test-serialization:
needs:
- detect-python-versions
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,13 @@ unit_test_extras_codecov:
unit_test:
# Skip all extra tests and run them with the necessary env var set so that a working (albeit slower)
# library is used to serialize/deserialize protobufs is used.
$(PYTEST_AND_OPTS) -m "not (serial or sandbox_test)" tests/flytekit/unit/ --ignore=tests/flytekit/unit/extras/ --ignore=tests/flytekit/unit/models --ignore=tests/flytekit/unit/extend ${CODECOV_OPTS}
$(PYTEST_AND_OPTS) -m "not (serial or sandbox_test or hypothesis)" tests/flytekit/unit/ --ignore=tests/flytekit/unit/extras/ --ignore=tests/flytekit/unit/models --ignore=tests/flytekit/unit/extend ${CODECOV_OPTS}
# Run serial tests without any parallelism
$(PYTEST) -m "serial" tests/flytekit/unit/ --ignore=tests/flytekit/unit/extras/ --ignore=tests/flytekit/unit/models --ignore=tests/flytekit/unit/extend ${CODECOV_OPTS}

.PHONY: unit_test_hypothesis
unit_test_hypothesis:
$(PYTEST_AND_OPTS) -m "hypothesis" tests/flytekit/unit/experimental ${CODECOV_OPTS}

.PHONY: unit_test_extras
unit_test_extras:
Expand Down
22 changes: 14 additions & 8 deletions flytekit/core/python_function_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,15 +349,21 @@ def dynamic_execute(self, task_function: Callable, **kwargs) -> Any:
raise ValueError(f"Invalid execution provided, execution state: {ctx.execution_state}")

def _write_decks(self, native_inputs, native_outputs_as_map, ctx, new_user_params):
# These errors are raised if the source code can not be retrieved
with suppress(OSError, TypeError):
source_code = inspect.getsource(self._task_function)

if self._disable_deck is False:
from flytekit.deck import Deck
from flytekit.deck.renderer import SourceCodeRenderer
from flytekit.deck.renderer import PythonDependencyRenderer

# These errors are raised if the source code can not be retrieved
with suppress(OSError, TypeError):
source_code = inspect.getsource(self._task_function)
from flytekit.deck.renderer import SourceCodeRenderer

source_code_deck = Deck("Source Code")
renderer = SourceCodeRenderer()
source_code_deck.append(renderer.to_html(source_code))

source_code_deck = Deck("Source Code")
renderer = SourceCodeRenderer()
source_code_deck.append(renderer.to_html(source_code))
python_dependencies_deck = Deck("Dependencies")
renderer = PythonDependencyRenderer()
python_dependencies_deck.append(renderer.to_html())

return super()._write_decks(native_inputs, native_outputs_as_map, ctx, new_user_params)
74 changes: 74 additions & 0 deletions flytekit/deck/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,77 @@
css = formatter.get_style_defs(".highlight").replace("#fff0f0", "#ffffff")
html = highlight(source_code, PythonLexer(), formatter)
return f"<style>{css}</style>{html}"


class PythonDependencyRenderer:
"""
PythonDependencyDeck is a deck that contains information about packages installed via pip.
"""

def __init__(self, title: str = "Dependencies"):
self._title = title

def to_html(self) -> str:
import json
import subprocess
import sys

from flytekit.loggers import logger

try:
installed_packages = json.loads(
subprocess.check_output([sys.executable, "-m", "pip", "list", "--format", "json"])
)
requirements_txt = (
subprocess.check_output([sys.executable, "-m", "pip", "freeze"])
.decode("utf-8")
.replace("\\n", "\n")
.rstrip()
)
except subprocess.CalledProcessError as e:
logger.error(f"Error occurred while fetching installed packages: {e}")
return "Error occurred while fetching installed packages."

Check warning on line 118 in flytekit/deck/renderer.py

View check run for this annotation

Codecov / codecov/patch

flytekit/deck/renderer.py#L116-L118

Added lines #L116 - L118 were not covered by tests

table = (
"<table>\n<tr>\n<th style='text-align:left;'>Name</th>\n<th style='text-align:left;'>Version</th>\n</tr>\n"
)

for entry in installed_packages:
table += f"<tr>\n<td>{entry['name']}</td>\n<td>{entry['version']}</td>\n</tr>\n"

table += "</table>"

html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flyte Dependencies</title>
<script>
async function copyTable() {{
var requirements_txt = document.getElementById('requirements_txt');

try {{
await navigator.clipboard.writeText(requirements_txt.innerText);
}} catch (err) {{
console.log('Error accessing the clipboard: ' + err);
}}
}}
</script>
</head>
<body>

<button onclick="copyTable()">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eapolinario This copy button does not work. I get this error in the console:

Screenshot 2024-04-02 at 3 14 42 PM

Screenshot 2024-04-02 at 3 15 38 PM

If we can not get this to work, I'm okay with removing the copy button and just showing the table.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is blocked on flyteorg/flyteconsole#852.

Copy link
Collaborator

@eapolinario eapolinario Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That just got merged, so next Flyte release is going to have this.

<span>Copy table as requirements.txt</span>
</button>
<h3>Python Dependencies</h3>

{table}

<div id="requirements_txt" style="display:none">{requirements_txt}</div>

</body>
</html>
"""
return html
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ log_cli_level = 20
markers = [
"sandbox_test: fake integration tests", # unit tests that are really integration tests that run on a sandbox environment
"serial: tests to avoid using with pytest-xdist",
"hypothesis: tests that use they hypothesis library",
]

[tool.coverage.report]
Expand Down
9 changes: 9 additions & 0 deletions tests/flytekit/unit/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import os

import pytest
from hypothesis import settings

from flytekit.image_spec.image_spec import ImageSpecBuilder

Expand All @@ -11,3 +14,9 @@ def build_image(self, img):
@pytest.fixture()
def mock_image_spec_builder():
return MockImageSpecBuilder()


settings.register_profile("ci", max_examples=5, deadline=100_000)
settings.register_profile("dev", max_examples=10, deadline=10_000)

settings.load_profile(os.getenv("FLYTEKIT_HYPOTHESIS_PROFILE", "dev"))
45 changes: 36 additions & 9 deletions tests/flytekit/unit/deck/test_deck.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@

import pytest
from markdown_it import MarkdownIt
from mock import mock
from mock import mock, patch

import flytekit
from flytekit import Deck, FlyteContextManager, task
from flytekit.deck import MarkdownRenderer, SourceCodeRenderer, TopFrameRenderer
from flytekit.deck.deck import _output_deck
from flytekit.deck.renderer import PythonDependencyRenderer


@pytest.mark.skipif("pandas" not in sys.modules, reason="Pandas is not installed.")
Expand Down Expand Up @@ -50,9 +51,9 @@ def test_timeline_deck():
@pytest.mark.parametrize(
"disable_deck,expected_decks",
[
(None, 2), # time line deck + source code deck
(False, 4), # time line deck + source code deck + input and output decks
(True, 2), # time line deck + source code deck
(None, 1), # time line deck
(False, 5), # time line deck + source code deck + python dependency deck + input and output decks
(True, 1), # time line deck
],
)
def test_deck_for_task(disable_deck, expected_decks):
Expand All @@ -75,11 +76,21 @@ def t1(a: int) -> str:
@pytest.mark.parametrize(
"enable_deck,disable_deck, expected_decks, expect_error",
[
(None, None, 3, False), # default deck and time line deck + source code deck
(None, False, 5, False), # default deck and time line deck + source code deck + input and output decks
(None, True, 3, False), # default deck and time line deck + source code deck
(True, None, 5, False), # default deck and time line deck + source code deck + input and output decks
(False, None, 3, False), # default deck and time line deck + source code deck
(None, None, 2, False), # default deck and time line deck
(
None,
False,
6,
False,
), # default deck and time line deck + source code deck + python dependency deck + input and output decks
(None, True, 2, False), # default deck and time line deck
(
True,
None,
6,
False,
), # default deck and time line deck + source code deck + python dependency deck + input and output decks
(False, None, 2, False), # default deck and time line deck
(True, True, -1, True), # Set both disable_deck and enable_deck to True and confirm that it fails
(False, False, -1, True), # Set both disable_deck and enable_deck to False and confirm that it fails
],
Expand Down Expand Up @@ -176,3 +187,19 @@ def test_source_code_renderer():
# Assert that the color #ffffff is used instead of #fff0f0
assert "#ffffff" in result
assert "#fff0f0" not in result


def test_python_dependency_renderer():
with patch("subprocess.check_output") as mock_check_output:
mock_check_output.return_value = '[{"name": "numpy", "version": "1.21.0"}]'.encode()
renderer = PythonDependencyRenderer()
result = renderer.to_html()
assert "numpy" in result
assert "1.21.0" in result

# Assert that the result includes parts of the python dependency
assert "Name" in result
assert "Version" in result

# Assert that the button of copy
assert 'button onclick="copyTable()"' in result
24 changes: 12 additions & 12 deletions tests/flytekit/unit/experimental/test_eager_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import hypothesis.strategies as st
import pytest
from hypothesis import given, settings
from hypothesis import given

from flytekit import dynamic, task, workflow
from flytekit.exceptions.user import FlyteValidationException
Expand All @@ -15,7 +15,6 @@
from flytekit.types.file import FlyteFile
from flytekit.types.structured import StructuredDataset

DEADLINE = 2000
INTEGER_ST = st.integers(min_value=-10_000_000, max_value=10_000_000)


Expand Down Expand Up @@ -48,7 +47,7 @@ def dynamic_wf(x: int) -> int:


@given(x_input=INTEGER_ST)
@settings(deadline=DEADLINE, max_examples=5)
@pytest.mark.hypothesis
def test_simple_eager_workflow(x_input: int):
"""Testing simple eager workflow with just tasks."""

Expand All @@ -62,7 +61,7 @@ async def eager_wf(x: int) -> int:


@given(x_input=INTEGER_ST)
@settings(deadline=DEADLINE, max_examples=5)
@pytest.mark.hypothesis
def test_conditional_eager_workflow(x_input: int):
"""Test eager workflow with conditional logic."""

Expand All @@ -80,7 +79,7 @@ async def eager_wf(x: int) -> int:


@given(x_input=INTEGER_ST)
@settings(deadline=DEADLINE, max_examples=5)
@pytest.mark.hypothesis
def test_try_except_eager_workflow(x_input: int):
"""Test eager workflow with try/except logic."""

Expand All @@ -99,7 +98,7 @@ async def eager_wf(x: int) -> int:


@given(x_input=INTEGER_ST, n_input=st.integers(min_value=1, max_value=20))
@settings(deadline=DEADLINE, max_examples=5)
@pytest.mark.hypothesis
def test_gather_eager_workflow(x_input: int, n_input: int):
"""Test eager workflow with asyncio gather."""

Expand All @@ -113,7 +112,7 @@ async def eager_wf(x: int, n: int) -> typing.List[int]:


@given(x_input=INTEGER_ST)
@settings(deadline=DEADLINE, max_examples=5)
@pytest.mark.hypothesis
def test_eager_workflow_with_dynamic_exception(x_input: int):
"""Test eager workflow with dynamic workflow is not supported."""

Expand All @@ -131,7 +130,7 @@ async def nested_eager_wf(x: int) -> int:


@given(x_input=INTEGER_ST)
@settings(deadline=DEADLINE, max_examples=5)
@pytest.mark.hypothesis
def test_nested_eager_workflow(x_input: int):
"""Testing running nested eager workflows."""

Expand All @@ -145,7 +144,7 @@ async def eager_wf(x: int) -> int:


@given(x_input=INTEGER_ST)
@settings(deadline=DEADLINE, max_examples=5)
@pytest.mark.hypothesis
def test_eager_workflow_within_workflow(x_input: int):
"""Testing running eager workflow within a static workflow."""

Expand All @@ -168,7 +167,7 @@ def subworkflow(x: int) -> int:


@given(x_input=INTEGER_ST)
@settings(deadline=DEADLINE, max_examples=5)
@pytest.mark.hypothesis
def test_workflow_within_eager_workflow(x_input: int):
"""Testing running a static workflow within an eager workflow."""

Expand All @@ -182,7 +181,7 @@ async def eager_wf(x: int) -> int:


@given(x_input=INTEGER_ST)
@settings(deadline=DEADLINE, max_examples=5)
@pytest.mark.hypothesis
def test_local_task_eager_workflow_exception(x_input: int):
"""Testing simple eager workflow with a local function task doesn't work."""

Expand All @@ -199,8 +198,8 @@ async def eager_wf_with_local(x: int) -> int:


@given(x_input=INTEGER_ST)
@settings(deadline=DEADLINE, max_examples=5)
@pytest.mark.filterwarnings("ignore:coroutine 'AsyncEntity.__call__' was never awaited")
@pytest.mark.hypothesis
def test_local_workflow_within_eager_workflow_exception(x_input: int):
"""Cannot call a locally-defined workflow within an eager workflow"""

Expand Down Expand Up @@ -243,6 +242,7 @@ def create_directory() -> FlyteDirectory:


@pytest.mark.skipif("pandas" not in sys.modules, reason="Pandas is not installed.")
@pytest.mark.hypothesis
def test_eager_workflow_with_offloaded_types():
"""Test eager workflow that eager workflows work with offloaded types."""
import pandas as pd
Expand Down
Loading